From bf84e2fa7f38ad409af82f4a8523d06cb77039bc Mon Sep 17 00:00:00 2001 From: 5p2O5pe25ouT <245786660@qq.com> Date: Tue, 29 Aug 2023 23:51:27 +0800 Subject: [PATCH] Add registry certificate verification support (#1232) * add registry certificate verification support * modify go.mod * rename registry cert options, add docs, and add test Signed-off-by: Alex Goodman * update to account for changes in anchore/stereoscope#195 Signed-off-by: Alex Goodman * fix cli tests Signed-off-by: Alex Goodman --------- Signed-off-by: Alex Goodman Co-authored-by: lishituo <24578666@qq.com> Co-authored-by: Alex Goodman --- .gitignore | 1 + README.md | 32 ++++++++++++++---- go.mod | 27 +++++++-------- go.sum | 4 +-- internal/config/registry.go | 34 ++++++++++++++----- internal/config/registry_test.go | 58 ++++++++++++++++++++++++++------ test/cli/registry_auth_test.go | 15 +++++++-- 7 files changed, 125 insertions(+), 46 deletions(-) diff --git a/.gitignore b/.gitignore index 038a3818f29..d75b1549039 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /go.work /go.work.sum +/.grype.yaml CHANGELOG.md VERSION diff --git a/README.md b/README.md index d97c1f3f0d5..7f3b11b3761 100644 --- a/README.md +++ b/README.md @@ -651,23 +651,41 @@ registry: # skip TLS verification when communicating with the registry # same as GRYPE_REGISTRY_INSECURE_SKIP_TLS_VERIFY env var insecure-skip-tls-verify: false + # use http instead of https when connecting to the registry # same as GRYPE_REGISTRY_INSECURE_USE_HTTP env var insecure-use-http: false + # filepath to a CA certificate (or directory containing *.crt, *.cert, *.pem) used to generate the client certificate + # GRYPE_REGISTRY_CA_CERT env var + ca-cert: "" + # credentials for specific registries auth: - - # the URL to the registry (e.g. "docker.io", "localhost:5000", etc.) - # same as GRYPE_REGISTRY_AUTH_AUTHORITY env var - authority: "" - # same as GRYPE_REGISTRY_AUTH_USERNAME env var + # the URL to the registry (e.g. "docker.io", "localhost:5000", etc.) + # GRYPE_REGISTRY_AUTH_AUTHORITY env var + - authority: "" + + # GRYPE_REGISTRY_AUTH_USERNAME env var username: "" - # same as GRYPE_REGISTRY_AUTH_PASSWORD env var + + # GRYPE_REGISTRY_AUTH_PASSWORD env var password: "" + # note: token and username/password are mutually exclusive - # same as GRYPE_REGISTRY_AUTH_TOKEN env var + # GRYPE_REGISTRY_AUTH_TOKEN env var token: "" - - ... # note, more credentials can be provided via config file only + + # filepath to the client certificate used for TLS authentication to the registry + # GRYPE_REGISTRY_AUTH_TLS_CERT env var + tls-cert: "" + + # filepath to the client key used for TLS authentication to the registry + # GRYPE_REGISTRY_AUTH_TLS_KEY env var + tls-key: "" + + # - ... # note, more credentials can be provided via config file only (not env vars) + log: # use structured logging diff --git a/go.mod b/go.mod index a4148fa8c38..6ec8e61e031 100644 --- a/go.mod +++ b/go.mod @@ -7,19 +7,28 @@ require ( github.com/Masterminds/sprig/v3 v3.2.3 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/adrg/xdg v0.4.0 + github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461 + github.com/anchore/clio v0.0.0-20230630162005-9535e9dc2817 + github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501 - github.com/anchore/stereoscope v0.0.0-20230727211946-d1f3d766295e + github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963 + github.com/anchore/stereoscope v0.0.0-20230829142608-334c2222e137 + github.com/anchore/syft v0.88.0 github.com/bmatcuk/doublestar/v2 v2.0.4 + github.com/charmbracelet/bubbletea v0.24.2 + github.com/charmbracelet/lipgloss v0.8.0 github.com/docker/docker v24.0.5+incompatible github.com/dustin/go-humanize v1.0.1 github.com/facebookincubator/nvdtools v0.1.5 github.com/gabriel-vasile/mimetype v1.4.2 + github.com/gkampitakis/go-snaps v0.4.8 github.com/go-test/deep v1.1.0 github.com/google/go-cmp v0.5.9 github.com/google/uuid v1.3.1 github.com/gookit/color v1.5.4 + github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-getter v1.7.2 github.com/hashicorp/go-multierror v1.1.1 @@ -29,6 +38,7 @@ require ( github.com/mholt/archiver/v3 v3.5.1 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/hashstructure/v2 v2.0.2 + github.com/mitchellh/mapstructure v1.5.0 github.com/olekukonko/tablewriter v0.0.5 github.com/owenrumney/go-sarif v1.1.1 github.com/pkg/profile v1.7.0 @@ -42,6 +52,7 @@ require ( github.com/spf13/viper v1.16.0 github.com/stretchr/testify v1.8.4 github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 + github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5 github.com/x-cray/logrus-prefixed-formatter v0.5.2 golang.org/x/term v0.11.0 @@ -49,20 +60,6 @@ require ( gorm.io/gorm v1.23.10 ) -require ( - github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461 - github.com/anchore/clio v0.0.0-20230630162005-9535e9dc2817 - github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe - github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963 - github.com/anchore/syft v0.88.0 - github.com/charmbracelet/bubbletea v0.24.2 - github.com/charmbracelet/lipgloss v0.8.0 - github.com/gkampitakis/go-snaps v0.4.8 - github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b - github.com/mitchellh/mapstructure v1.5.0 - github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b -) - require ( cloud.google.com/go v0.110.0 // indirect cloud.google.com/go/compute v1.19.3 // indirect diff --git a/go.sum b/go.sum index eea219a9b17..16eca7a8f9e 100644 --- a/go.sum +++ b/go.sum @@ -249,8 +249,8 @@ github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501 h1:AV7qjwM github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4= github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963 h1:vrf2PYH77vqVJoNR15ZuFJ63qwBMqrmGIt/7VsBhLF8= github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963/go.mod h1:AVRyXOUP0hTz9Cb8OlD1XnwA8t4lBPfTuwPHmEUuiLc= -github.com/anchore/stereoscope v0.0.0-20230727211946-d1f3d766295e h1:S6IhYpsBCpvphlHA1tN0glSG/kjVvFzC6OJuU2qW5Pc= -github.com/anchore/stereoscope v0.0.0-20230727211946-d1f3d766295e/go.mod h1:0LsgHgXO4QFnk2hsYwtqd3fR18PIZXlFLIl2qb9tu3g= +github.com/anchore/stereoscope v0.0.0-20230829142608-334c2222e137 h1:ZuiAV3lYKbGPkZR42UpoKecp/9aFU38CZSjaxjXCMYc= +github.com/anchore/stereoscope v0.0.0-20230829142608-334c2222e137/go.mod h1:jkylrnuv++srUa2rsqCnG0n1ShD+0k2Xa6YtoNoRjAY= github.com/anchore/syft v0.88.0 h1:QRPcXwbQnxcOIfSZ5Sd6psfVQ756VICvx/HUMsIJEBw= github.com/anchore/syft v0.88.0/go.mod h1:6GgbZflKWC7ph2Zjb5wgq0ORKhwDaHG3xcjG84FSMPo= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= diff --git a/internal/config/registry.go b/internal/config/registry.go index b2a89155ed4..c8453b91195 100644 --- a/internal/config/registry.go +++ b/internal/config/registry.go @@ -16,30 +16,37 @@ type RegistryCredentials struct { Password string `yaml:"-" json:"-" mapstructure:"password"` // IMPORTANT: do not show the token in any YAML/JSON output (sensitive information) Token string `yaml:"-" json:"-" mapstructure:"token"` + + TLSCert string `yaml:"tls-cert,omitempty" json:"tls-cert,omitempty" mapstructure:"tls-cert"` + TLSKey string `yaml:"tls-key,omitempty" json:"tls-key,omitempty" mapstructure:"tls-key"` } type registry struct { InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"` InsecureUseHTTP bool `yaml:"insecure-use-http" json:"insecure-use-http" mapstructure:"insecure-use-http"` Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"` + CACert string `yaml:"ca-cert" json:"ca-cert" mapstructure:"ca-cert"` } func (cfg registry) loadDefaultValues(v *viper.Viper) { v.SetDefault("registry.insecure-skip-tls-verify", false) v.SetDefault("registry.insecure-use-http", false) v.SetDefault("registry.auth", []RegistryCredentials{}) + v.SetDefault("registry.ca-cert", "") } //nolint:unparam func (cfg *registry) parseConfigValues() error { // there may be additional credentials provided by env var that should be appended to the set of credentials - authority, username, password, token := + authority, username, password, token, tlsCert, tlsKey := os.Getenv("GRYPE_REGISTRY_AUTH_AUTHORITY"), os.Getenv("GRYPE_REGISTRY_AUTH_USERNAME"), os.Getenv("GRYPE_REGISTRY_AUTH_PASSWORD"), - os.Getenv("GRYPE_REGISTRY_AUTH_TOKEN") + os.Getenv("GRYPE_REGISTRY_AUTH_TOKEN"), + os.Getenv("GRYPE_REGISTRY_AUTH_TLS_CERT"), + os.Getenv("GRYPE_REGISTRY_AUTH_TLS_KEY") - if hasNonEmptyCredentials(username, password, token) { + if hasNonEmptyCredentials(username, password, token, tlsCert, tlsKey) { // note: we prepend the credentials such that the environment variables take precedence over on-disk configuration. cfg.Auth = append([]RegistryCredentials{ { @@ -47,29 +54,38 @@ func (cfg *registry) parseConfigValues() error { Username: username, Password: password, Token: token, + TLSCert: tlsCert, + TLSKey: tlsKey, }, }, cfg.Auth...) } return nil } -func hasNonEmptyCredentials(username, password, token string) bool { - return password != "" && username != "" || token != "" +func hasNonEmptyCredentials(username, password, token, tlsCert, tlsKey string) bool { + hasUserPass := username != "" && password != "" + hasToken := token != "" + hasTLSMaterial := tlsCert != "" && tlsKey != "" + return hasUserPass || hasToken || hasTLSMaterial } func (cfg *registry) ToOptions() *image.RegistryOptions { var auth = make([]image.RegistryCredentials, len(cfg.Auth)) for i, a := range cfg.Auth { auth[i] = image.RegistryCredentials{ - Authority: a.Authority, - Username: a.Username, - Password: a.Password, - Token: a.Token, + Authority: a.Authority, + Username: a.Username, + Password: a.Password, + Token: a.Token, + ClientCert: a.TLSCert, + ClientKey: a.TLSKey, } } + return &image.RegistryOptions{ InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify, InsecureUseHTTP: cfg.InsecureUseHTTP, Credentials: auth, + CAFileOrDir: cfg.CACert, } } diff --git a/internal/config/registry_test.go b/internal/config/registry_test.go index d14769bcf18..034384e6161 100644 --- a/internal/config/registry_test.go +++ b/internal/config/registry_test.go @@ -11,47 +11,60 @@ import ( func TestHasNonEmptyCredentials(t *testing.T) { tests := []struct { - username, password, token string - expected bool + username, password, token, cert, key string + expected bool }{ + { - "", "", "", + "", "", "", "", "", false, }, { - "user", "", "", + "user", "", "", "", "", false, }, { - "", "pass", "", + "", "pass", "", "", "", false, }, { - "", "pass", "tok", + "", "pass", "tok", "", "", true, }, { - "user", "", "tok", + "user", "", "tok", "", "", true, }, { - "", "", "tok", + "", "", "tok", "", "", true, }, { - "user", "pass", "tok", + "user", "pass", "tok", "", "", true, }, { - "user", "pass", "", + "user", "pass", "", "", "", true, }, + { + "", "", "", "cert", "key", + true, + }, + { + "", "", "", "cert", "", + false, + }, + { + "", "", "", "", "key", + false, + }, } for _, test := range tests { t.Run(fmt.Sprintf("%+v", test), func(t *testing.T) { - assert.Equal(t, test.expected, hasNonEmptyCredentials(test.username, test.password, test.token)) + assert.Equal(t, test.expected, hasNonEmptyCredentials(test.username, test.password, test.token, test.cert, test.key)) }) } } @@ -101,6 +114,29 @@ func Test_registry_ToOptions(t *testing.T) { Credentials: []image.RegistryCredentials{}, }, }, + { + name: "provide all tls configuration", + input: registry{ + CACert: "ca.crt", + InsecureSkipTLSVerify: true, + Auth: []RegistryCredentials{ + { + TLSCert: "client.crt", + TLSKey: "client.key", + }, + }, + }, + expected: image.RegistryOptions{ + CAFileOrDir: "ca.crt", + InsecureSkipTLSVerify: true, + Credentials: []image.RegistryCredentials{ + { + ClientCert: "client.crt", + ClientKey: "client.key", + }, + }, + }, + }, } for _, test := range tests { diff --git a/test/cli/registry_auth_test.go b/test/cli/registry_auth_test.go index a06c28a041c..0d6b0c8828b 100644 --- a/test/cli/registry_auth_test.go +++ b/test/cli/registry_auth_test.go @@ -18,7 +18,7 @@ func TestRegistryAuth(t *testing.T) { assertions: []traitAssertion{ assertInOutput("source=OciRegistry"), assertInOutput("localhost:5000/something:latest"), - assertInOutput("no registry credentials configured, using the default keychain"), + assertInOutput(`no registry credentials configured for "localhost:5000", using the default keychain`), }, }, { @@ -57,7 +57,7 @@ func TestRegistryAuth(t *testing.T) { assertions: []traitAssertion{ assertInOutput("source=OciRegistry"), assertInOutput("localhost:5000/something:latest"), - assertInOutput(`no registry credentials configured, using the default keychain`), + assertInOutput(`no registry credentials configured for "localhost:5000", using the default keychain`), }, }, { @@ -70,6 +70,17 @@ func TestRegistryAuth(t *testing.T) { assertInOutput("insecure-use-http: true"), }, }, + { + name: "use tls configuration", + args: []string{"-vvv", "registry:localhost:5000/something:latest"}, + env: map[string]string{ + "GRYPE_REGISTRY_AUTH_TLS_CERT": "place.crt", + "GRYPE_REGISTRY_AUTH_TLS_KEY": "place.key", + }, + assertions: []traitAssertion{ + assertInOutput("using custom TLS credentials from"), + }, + }, } for _, test := range tests {