Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for registry mirrors #8244

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions docs/docs/configuration/others.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,46 @@ The following example will fail when a critical vulnerability is found or the OS
```
$ trivy image --exit-code 1 --exit-on-eol 1 --severity CRITICAL alpine:3.16.3
```

## Mirror Registries

!!! warning "EXPERIMENTAL"
This feature might change without preserving backwards compatibility.

Trivy supports mirrors for [remote container images](../target/container_image.md#container-registry) and [databases](./db.md).

To configure them, add a list of mirrors along with the host to the [trivy config file](../references/configuration/config-file.md#registry-options).
knqyf263 marked this conversation as resolved.
Show resolved Hide resolved

!!! note
Use the `index.docker.io` host for images from `Docker Hub`, even if you don't use that prefix.

Example for `index.docker.io`:
```yaml
registry:
mirrors:
index.docker.io:
- mirror.gcr.io
```

### Registry check procedure
Trivy uses the following registry order to get the image:

- mirrors in the same order as they are specified in the configuration file
- source registry

In cases where we can't get the image from the mirror registry (e.g. when authentication fails, image doesn't exist, etc.) - Trivy will check other mirrors (or the source registry if all mirrors have already been checked).

Example:
```yaml
registry:
mirrors:
index.docker.io:
- mirror.with.bad.auth // We don't have credentials for this registry
- mirror.without.image // Registry doesn't have this image
```

When we want to get the image `alpine` with the settings above. The logic will be as follows:

1. Try to get the image from `mirror.with.bad.auth/library/alpine`, but we get an error because there are no credentials for this registry.
2. Try to get the image from `mirror.without.image/library/alpine`, but we get an error because this registry doesn't have this image (but most likely it will be an error about authorization).
3. Get the image from `index.docker.io` (the original registry).
5 changes: 5 additions & 0 deletions docs/docs/references/configuration/config-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,11 @@ pkg:

```yaml
registry:
mirrors:
index.docker.io:
- harbor.example.com/docker.io
- mirror.gcr.io
Comment on lines +464 to +467
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, this page shows the default values. I'm wondering if it confuses users. Since the document describes an example, isn't it enough?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are not adding flags support, so we need to include something in the config file docs.
But honestly, I added this example because I could come up with good lines for an empty map[string][]string

This is a yaml file, and the go (map[string][]string) syntax can be confusing to users.

I can use <..> in the example. eg:

mirrors:
  <origin_registry>:
    - <first_mirror>
    - <second_mirror>

let me know if you have another idea.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This document well describes how the configuration should look.
https://github.com/aquasecurity/trivy/blob/f3247f157a54cc050d66db7627164326b0dae607/docs/docs/configuration/others.md#mirror-registries

IMHO, config-file.md can be simply mirros: because it's the default value.

  mirrors:

But your approach also looks good.

mirrors:
  <origin_registry>:
    - <first_mirror>
    - <second_mirror>

In that case, we may want to add a new field for examples and generate config-file.md from them rather than defining specific examples in magefiles


# Same as '--password'
password: []

Expand Down
24 changes: 23 additions & 1 deletion magefiles/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func writeFlags(group flag.FlagGroup, w *os.File) {
}
w.WriteString(ind + parts[i] + ":")
if isLastPart {
writeFlagValue(flg.GetDefaultValue(), ind, w)
writeFlagValue(value(flg), ind, w)
}
w.WriteString("\n")
}
Expand All @@ -147,9 +147,31 @@ func writeFlagValue(val any, ind string, w *os.File) {
} else {
w.WriteString(" []\n")
}
case map[string][]string:
w.WriteString("\n")
for k, vv := range v {
fmt.Fprintf(w, "%s %s:\n", ind, k)
for _, vvv := range vv {
fmt.Fprintf(w, " %s - %s\n", ind, vvv)
}
}
case string:
fmt.Fprintf(w, " %q\n", v)
default:
fmt.Fprintf(w, " %v\n", v)
}
}

var registryMirrorsExample = map[string][]string{
"index.docker.io": {
"harbor.example.com/docker.io",
"mirror.gcr.io",
},
}

func value(flg flag.Flagger) any {
if flg.GetConfigName() == flag.RegistryMirrorsFlag.ConfigName {
return registryMirrorsExample
}
return flg.GetDefaultValue()
}
3 changes: 3 additions & 0 deletions pkg/fanal/types/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ type RegistryOptions struct {
// RegistryToken is a bearer token to be sent to a registry
RegistryToken string

// RegistryMirrors is a map of hosts with mirrors for them
RegistryMirrors map[string][]string

// SSL/TLS
Insecure bool

Expand Down
15 changes: 9 additions & 6 deletions pkg/flag/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
)

type FlagType interface {
int | string | []string | bool | time.Duration | float64
int | string | []string | bool | time.Duration | float64 | map[string][]string
}

type Flag[T FlagType] struct {
Expand Down Expand Up @@ -161,6 +161,8 @@ func (f *Flag[T]) cast(val any) any {
return cast.ToFloat64(val)
case time.Duration:
return cast.ToDuration(val)
case map[string][]string:
return cast.ToStringMapStringSlice(val)
Comment on lines +164 to +165
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm impressed it works!

case []string:
if s, ok := val.(string); ok && strings.Contains(s, ",") {
// Split environmental variables by comma as it is not done by viper.
Expand Down Expand Up @@ -467,11 +469,12 @@ func (o *Options) ScanOpts() types.ScanOptions {
// RegistryOpts returns options for OCI registries
func (o *Options) RegistryOpts() ftypes.RegistryOptions {
return ftypes.RegistryOptions{
Credentials: o.Credentials,
RegistryToken: o.RegistryToken,
Insecure: o.Insecure,
Platform: o.Platform,
AWSRegion: o.AWSOptions.Region,
Credentials: o.Credentials,
RegistryToken: o.RegistryToken,
Insecure: o.Insecure,
Platform: o.Platform,
AWSRegion: o.AWSOptions.Region,
RegistryMirrors: o.RegistryMirrors,
}
}

Expand Down
33 changes: 21 additions & 12 deletions pkg/flag/registry_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,33 @@ var (
ConfigName: "registry.token",
Usage: "registry token",
}
RegistryMirrorsFlag = Flag[map[string][]string]{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smart 👏

ConfigName: "registry.mirrors",
Usage: "map of hosts and registries for them.",
}
)

type RegistryFlagGroup struct {
Username *Flag[[]string]
Password *Flag[[]string]
PasswordStdin *Flag[bool]
RegistryToken *Flag[string]
Username *Flag[[]string]
Password *Flag[[]string]
PasswordStdin *Flag[bool]
RegistryToken *Flag[string]
RegistryMirrors *Flag[map[string][]string]
}

type RegistryOptions struct {
Credentials []types.Credential
RegistryToken string
Credentials []types.Credential
RegistryToken string
RegistryMirrors map[string][]string
}

func NewRegistryFlagGroup() *RegistryFlagGroup {
return &RegistryFlagGroup{
Username: UsernameFlag.Clone(),
Password: PasswordFlag.Clone(),
PasswordStdin: PasswordStdinFlag.Clone(),
RegistryToken: RegistryTokenFlag.Clone(),
Username: UsernameFlag.Clone(),
Password: PasswordFlag.Clone(),
PasswordStdin: PasswordStdinFlag.Clone(),
RegistryToken: RegistryTokenFlag.Clone(),
RegistryMirrors: RegistryMirrorsFlag.Clone(),
}
}

Expand All @@ -64,6 +71,7 @@ func (f *RegistryFlagGroup) Flags() []Flagger {
f.Password,
f.PasswordStdin,
f.RegistryToken,
f.RegistryMirrors,
}
}

Expand Down Expand Up @@ -97,7 +105,8 @@ func (f *RegistryFlagGroup) ToOptions() (RegistryOptions, error) {
}

return RegistryOptions{
Credentials: credentials,
RegistryToken: f.RegistryToken.Value(),
Credentials: credentials,
RegistryToken: f.RegistryToken.Value(),
RegistryMirrors: f.RegistryMirrors.Value(),
}, nil
}
91 changes: 85 additions & 6 deletions pkg/remote/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package remote
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"strings"
"time"

"github.com/google/go-containerregistry/pkg/authn"
Expand Down Expand Up @@ -35,8 +37,35 @@ func Get(ctx context.Context, ref name.Reference, option types.RegistryOptions)
return nil, xerrors.Errorf("failed to create http transport: %w", err)
}

mirrors, err := registryMirrors(ref, option)
if err != nil {
return nil, xerrors.Errorf("unable to parse mirrors: %w", err)
}

var errs error
// Try each mirrors/host until it succeeds
for _, r := range append(mirrors, ref) {
// Try each authentication method until it succeeds
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of nesting it, I prefer adding another method, like tryRef, tryImage or something like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated in ad56617

desc, err := tryGet(ctx, tr, r, option)
if err != nil {
var multiErr *multierror.Error
// authorization failed for all auth options - check other repositories
if errors.As(err, &multiErr) {
errs = multierror.Append(errs, multiErr.Errors...)
continue
}
// Other errors
return nil, err
}
return desc, nil
}

// No authentication for mirrors/host succeeded
return nil, errs
}

func tryGet(ctx context.Context, tr http.RoundTripper, ref name.Reference, option types.RegistryOptions) (*Descriptor, error) {
var errs error
// Try each authentication method until it succeeds
for _, authOpt := range authOptions(ctx, ref, option) {
remoteOpts := []remote.Option{
remote.WithTransport(tr),
Expand Down Expand Up @@ -65,10 +94,11 @@ func Get(ctx context.Context, ref name.Reference, option types.RegistryOptions)
return nil, err
}
}
if ref.Context().RegistryStr() != ref.Context().RegistryStr() {
log.WithPrefix("remote").Info("Mirror was used to get remote image", log.String("image", ref.String()), log.String("mirror", ref.Context().RegistryStr()))
}
return desc, nil
}

// No authentication succeeded
return nil, errs
}

Expand All @@ -80,8 +110,33 @@ func Image(ctx context.Context, ref name.Reference, option types.RegistryOptions
return nil, xerrors.Errorf("failed to create http transport: %w", err)
}

mirrors, err := registryMirrors(ref, option)
if err != nil {
return nil, xerrors.Errorf("unable to parse mirrors: %w", err)
}

var errs error
// Try each mirrors/origin registries until it succeeds
for _, r := range append(mirrors, ref) {
// Try each authentication method until it succeeds
var image v1.Image
image, err = tryImage(ctx, tr, r, option)
if err != nil {
errs = multierror.Append(errs, err)
continue

}
return image, nil
}

// No authentication for mirrors/host succeeded
return nil, errs
}

// tryImage checks all auth options and tries to get v1.Image.
// If none of the auth options work - function returns multierrors for each auth option.
func tryImage(ctx context.Context, tr http.RoundTripper, ref name.Reference, option types.RegistryOptions) (v1.Image, error) {
var errs error
// Try each authentication method until it succeeds
for _, authOpt := range authOptions(ctx, ref, option) {
remoteOpts := []remote.Option{
remote.WithTransport(tr),
Expand All @@ -92,10 +147,13 @@ func Image(ctx context.Context, ref name.Reference, option types.RegistryOptions
errs = multierror.Append(errs, err)
continue
}

if ref.Context().RegistryStr() != ref.Context().RegistryStr() {
log.WithPrefix("remote").Info("Mirror was used to get remote image",
log.String("image", ref.String()), log.String("mirror", ref.Context().RegistryStr()))
}
return index, nil
}

// No authentication succeeded
return nil, errs
}

Expand Down Expand Up @@ -126,6 +184,27 @@ func Referrers(ctx context.Context, d name.Digest, option types.RegistryOptions)
return nil, errs
}

func registryMirrors(hostRef name.Reference, option types.RegistryOptions) ([]name.Reference, error) {
var mirrors []name.Reference

reg := hostRef.Context().RegistryStr()
if ms, ok := option.RegistryMirrors[reg]; ok {
for _, m := range ms {
var nameOpts []name.Option
if option.Insecure {
nameOpts = append(nameOpts, name.Insecure)
}
mirrorImageName := strings.Replace(hostRef.Name(), reg, m, 1)
ref, err := name.ParseReference(mirrorImageName, nameOpts...)
if err != nil {
return nil, xerrors.Errorf("unable to parse image from mirror registry: %w", err)
}
mirrors = append(mirrors, ref)
}
}
return mirrors, nil
}

func httpTransport(option types.RegistryOptions) (http.RoundTripper, error) {
d := &net.Dialer{
Timeout: 10 * time.Minute,
Expand Down
Loading
Loading