mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 15:50:15 -08:00
feat: add support for registry mirrors (#8244)
Signed-off-by: knqyf263 <knqyf263@gmail.com> Co-authored-by: Teppei Fukuda <knqyf263@gmail.com>
This commit is contained in:
@@ -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).
|
||||
|
||||
!!! 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).
|
||||
|
||||
@@ -461,6 +461,8 @@ pkg:
|
||||
|
||||
```yaml
|
||||
registry:
|
||||
mirrors:
|
||||
|
||||
# Same as '--password'
|
||||
password: []
|
||||
|
||||
|
||||
@@ -147,6 +147,14 @@ 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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -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)
|
||||
case []string:
|
||||
if s, ok := val.(string); ok && strings.Contains(s, ",") {
|
||||
// Split environmental variables by comma as it is not done by viper.
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,26 +31,33 @@ var (
|
||||
ConfigName: "registry.token",
|
||||
Usage: "registry token",
|
||||
}
|
||||
RegistryMirrorsFlag = Flag[map[string][]string]{
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +71,7 @@ func (f *RegistryFlagGroup) Flags() []Flagger {
|
||||
f.Password,
|
||||
f.PasswordStdin,
|
||||
f.RegistryToken,
|
||||
f.RegistryMirrors,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ package remote
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
@@ -35,8 +37,14 @@ func Get(ctx context.Context, ref name.Reference, option types.RegistryOptions)
|
||||
return nil, xerrors.Errorf("failed to create http transport: %w", err)
|
||||
}
|
||||
|
||||
return tryWithMirrors(ref, option, func(r name.Reference) (*Descriptor, error) {
|
||||
return tryGet(ctx, tr, r, option)
|
||||
})
|
||||
}
|
||||
|
||||
// tryGet checks all auth options and tries to get Descriptor.
|
||||
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),
|
||||
@@ -67,8 +75,6 @@ func Get(ctx context.Context, ref name.Reference, option types.RegistryOptions)
|
||||
}
|
||||
return desc, nil
|
||||
}
|
||||
|
||||
// No authentication succeeded
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
@@ -80,8 +86,49 @@ func Image(ctx context.Context, ref name.Reference, option types.RegistryOptions
|
||||
return nil, xerrors.Errorf("failed to create http transport: %w", err)
|
||||
}
|
||||
|
||||
return tryWithMirrors(ref, option, func(r name.Reference) (v1.Image, error) {
|
||||
return tryImage(ctx, tr, r, option)
|
||||
})
|
||||
}
|
||||
|
||||
// tryWithMirrors handles common mirror logic for Get and Image functions
|
||||
func tryWithMirrors[T any](ref name.Reference, option types.RegistryOptions, fn func(name.Reference) (T, error)) (T, error) {
|
||||
var zero T
|
||||
mirrors, err := registryMirrors(ref, option)
|
||||
if err != nil {
|
||||
return zero, xerrors.Errorf("unable to parse mirrors: %w", err)
|
||||
}
|
||||
|
||||
// Try each mirrors/host until it succeeds
|
||||
var errs error
|
||||
for _, r := range append(mirrors, ref) {
|
||||
result, err := fn(r)
|
||||
if err != nil {
|
||||
var multiErr *multierror.Error
|
||||
// All auth options failed, try the next mirror/host
|
||||
if errors.As(err, &multiErr) {
|
||||
errs = multierror.Append(errs, multiErr.Errors...)
|
||||
continue
|
||||
}
|
||||
// Other errors
|
||||
return zero, err
|
||||
}
|
||||
|
||||
if ref.Context().RegistryStr() != r.Context().RegistryStr() {
|
||||
log.WithPrefix("remote").Info("Using the mirror registry to get the image",
|
||||
log.String("image", ref.String()), log.String("mirror", r.Context().RegistryStr()))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// No authentication for mirrors/host succeeded
|
||||
return zero, 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),
|
||||
@@ -92,10 +139,9 @@ func Image(ctx context.Context, ref name.Reference, option types.RegistryOptions
|
||||
errs = multierror.Append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
return index, nil
|
||||
}
|
||||
|
||||
// No authentication succeeded
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
@@ -126,6 +172,31 @@ func Referrers(ctx context.Context, d name.Digest, option types.RegistryOptions)
|
||||
return nil, errs
|
||||
}
|
||||
|
||||
// registryMirrors returns a list of mirrors for ref, obtained from options.RegistryMirrors
|
||||
// `go-containerregistry` doesn't support mirrors, so we need to handle them ourselves.
|
||||
// TODO: use `WithMirror` when `go-containerregistry` will support mirrors.
|
||||
// cf. https://github.com/google/go-containerregistry/pull/2010
|
||||
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,
|
||||
|
||||
@@ -93,6 +93,81 @@ func TestGet(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mirror",
|
||||
args: args{
|
||||
imageName: "foo.bar.io/library/alpine:3.10",
|
||||
option: types.RegistryOptions{
|
||||
Credentials: []types.Credential{
|
||||
{
|
||||
Username: "test",
|
||||
Password: "testpass",
|
||||
},
|
||||
},
|
||||
RegistryMirrors: map[string][]string{
|
||||
"foo.bar.io": {
|
||||
serverAddr,
|
||||
},
|
||||
},
|
||||
Insecure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mirror for dockerhub",
|
||||
args: args{
|
||||
imageName: "alpine:3.10",
|
||||
option: types.RegistryOptions{
|
||||
Credentials: []types.Credential{
|
||||
{
|
||||
Username: "test",
|
||||
Password: "testpass",
|
||||
},
|
||||
},
|
||||
RegistryMirrors: map[string][]string{
|
||||
"index.docker.io": {
|
||||
serverAddr,
|
||||
},
|
||||
},
|
||||
Insecure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-existent mirror image - use image from host",
|
||||
args: args{
|
||||
imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr),
|
||||
option: types.RegistryOptions{
|
||||
Credentials: []types.Credential{
|
||||
{
|
||||
Username: "test",
|
||||
Password: "testpass",
|
||||
},
|
||||
},
|
||||
RegistryMirrors: map[string][]string{
|
||||
serverAddr: {
|
||||
"wrong.repository",
|
||||
},
|
||||
},
|
||||
Insecure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wrong mirror",
|
||||
args: args{
|
||||
imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr),
|
||||
option: types.RegistryOptions{
|
||||
RegistryMirrors: map[string][]string{
|
||||
serverAddr: {
|
||||
"wrong.repository:tag@digest",
|
||||
},
|
||||
},
|
||||
Insecure: true,
|
||||
},
|
||||
},
|
||||
wantErr: "could not parse reference: wrong.repository:tag@digest/library/alpine:3.10",
|
||||
},
|
||||
{
|
||||
name: "multiple credential",
|
||||
args: args{
|
||||
@@ -182,6 +257,28 @@ func TestGet(t *testing.T) {
|
||||
},
|
||||
wantErr: "invalid username/password",
|
||||
},
|
||||
{
|
||||
name: "bad credential for multiple mirrors",
|
||||
args: args{
|
||||
imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr),
|
||||
option: types.RegistryOptions{
|
||||
Credentials: []types.Credential{
|
||||
{
|
||||
Username: "foo",
|
||||
Password: "bar",
|
||||
},
|
||||
},
|
||||
Insecure: true,
|
||||
RegistryMirrors: map[string][]string{
|
||||
serverAddr: {
|
||||
serverAddr,
|
||||
serverAddr,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: "6 errors occurred:", // 2 errors for each repository (for 2 mirrors and the original repository)
|
||||
},
|
||||
{
|
||||
name: "bad keychain",
|
||||
args: args{
|
||||
|
||||
Reference in New Issue
Block a user