Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
feat: Allow adding root certificate authorities
Harbor and Clair can be deployed with certificates signed by certificate
authorities which are not present in the adapters host's root CA set.
If that's the case, the corresponding clients will fail with well known
error: `certificate signed by unknown authority`. Also it happens
because the clients will not trust self-signed certificates, because
they don’t recognise the signer as a trusted Root CA.

In order to support custom CAs and self-signed certificates this commit
adds support for `SCANNER_TLS_CLIENTCAS` and
`SCANNER_TLS_INSECURE_SKIP_VERIFY` config envs.

Resolves: #2

Signed-off-by: Daniel Pacak <pacak.daniel@gmail.com>
  • Loading branch information
danielpacak committed Oct 25, 2019
commit a8c4249788b80bdb3463e1f966d19989121a6038
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ $(BINARY): $(SOURCES)

setup:
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s v1.21.0

.PHONY: setup

lint:
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Configuration of the adapter is done via environment variables at startup.
| `SCANNER_API_SERVER_ADDR` | `:8080` | Binding address for the API HTTP server. |
| `SCANNER_API_SERVER_TLS_CERTIFICATE` | | The absolute path to the x509 certificate file. |
| `SCANNER_API_SERVER_TLS_KEY` | | The absolute path to the x509 private key file. |
| `SCANNER_TLS_INSECURE_SKIP_VERIFY` | `false` | Controls whether an HTTP client verifies the server's certificate chain and host name. |
| `SCANNER_TLS_CLIENTCAS` | | An array of absolute paths to x509 CA files that will be added to host's root CA set. |
| `SCANNER_API_SERVER_READ_TIMEOUT` | `15s` | The maximum duration for reading the entire request, including the body. |
| `SCANNER_API_SERVER_WRITE_TIMEOUT` | `15s` | The maximum duration before timing out writes of the response. |
| `SCANNER_CLAIR_URL` | `http://harbor-harbor-clair:6060` | Clair URL |
Expand Down
9 changes: 7 additions & 2 deletions cmd/harbor-scanner-clair/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,13 @@ func main() {
log.Fatalf("Error: %v", err)
}

registryClientFactory := registry.NewClientFactory()
clairClient := clair.NewClient(clairConfig.URL)
tlsConfig, err := etc.GetTLSConfig()
if err != nil {
log.Fatalf("Error: %v", err)
}

registryClientFactory := registry.NewClientFactory(tlsConfig)
clairClient := clair.NewClient(tlsConfig, clairConfig)
scanner := clair.NewScanner(registryClientFactory, clairClient, model.NewTransformer())

apiConfig, err := etc.GetAPIConfig()
Expand Down
4 changes: 4 additions & 0 deletions kube/harbor-scanner-clair.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ spec:
value: "15s"
- name: "SCANNER_CLAIR_URL"
value: "http://harbor-harbor-clair:6060"
- name: "SCANNER_TLS_INSECURE_SKIP_VERIFY"
value: "false"
- name: "SCANNER_TLS_CLIENTCAS"
value: ""
ports:
- name: api-server-port
containerPort: 8443
Expand Down
42 changes: 42 additions & 0 deletions pkg/etc/config.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package etc

import (
"crypto/x509"
"fmt"
"github.com/caarlos0/env/v6"
"github.com/goharbor/harbor-scanner-clair/pkg/model/harbor"
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
"io/ioutil"
"os"
"strings"
"time"
)

Expand All @@ -20,6 +25,13 @@ func (c *APIConfig) IsTLSEnabled() bool {
return c.TLSCertificate != "" && c.TLSKey != ""
}

type TLSConfig struct {
ClientCAs []string `env:"SCANNER_TLS_CLIENTCAS"`
InsecureSkipVerify bool `env:"SCANNER_TLS_INSECURE_SKIP_VERIFY" envDefault:"false"`

RootCAs *x509.CertPool
}

type ClairConfig struct {
URL string `env:"SCANNER_CLAIR_URL" envDefault:"http://harbor-harbor-clair:6060"`
}
Expand All @@ -40,6 +52,36 @@ func GetAPIConfig() (cfg APIConfig, err error) {
return
}

func GetTLSConfig() (cfg TLSConfig, err error) {
err = env.Parse(&cfg)
if err != nil {
return
}

cfg.RootCAs, err = x509.SystemCertPool()
if err != nil {
log.WithError(err).Warn("Error while loading system root CAs")
}
if cfg.RootCAs == nil {
log.Debug("Creating empty root CAs pool")
cfg.RootCAs = x509.NewCertPool()
}

for _, certFile := range cfg.ClientCAs {
certs, err := ioutil.ReadFile(strings.TrimSpace(certFile))
if err != nil {
return cfg, fmt.Errorf("failed to append %q to root CAs pool: %v", certFile, err)
}

if ok := cfg.RootCAs.AppendCertsFromPEM(certs); !ok {
return cfg, fmt.Errorf("failed to append %q to root CAs pool: %v", certFile, err)
}
log.WithField("cert", certFile).Debug("Client CA appended to root CAs pool")
}

return
}

func GetClairConfig() (cfg ClairConfig, err error) {
err = env.Parse(&cfg)
return
Expand Down
33 changes: 33 additions & 0 deletions pkg/etc/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,39 @@ func TestAPIConfig_IsTLSEnabled(t *testing.T) {
}
}

func TestGetTLSConfig(t *testing.T) {
testCases := []struct {
name string
envs Envs
expectedConfig TLSConfig
}{
{
name: "Should return default config",
expectedConfig: TLSConfig{
InsecureSkipVerify: false,
},
},
{
name: "Should overwrite default config with envs",
envs: Envs{
"SCANNER_TLS_INSECURE_SKIP_VERIFY": "true",
"SCANNER_TLS_CLIENTCAS": "test/data/ca.crt",
},
expectedConfig: TLSConfig{
InsecureSkipVerify: true},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
setenvs(t, tc.envs)
// TODO Assert on the actual cfg and RootCAs
_, err := GetTLSConfig()
require.NoError(t, err)
})
}
}

func TestGetClairConfig(t *testing.T) {
testCases := []struct {
name string
Expand Down
18 changes: 18 additions & 0 deletions pkg/etc/test/data/ca.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC9DCCAdygAwIBAgIQIkbGQq4DnlKlorAYV5SjAzANBgkqhkiG9w0BAQsFADAU
MRIwEAYDVQQDEwloYXJib3ItY2EwHhcNMTkwOTMwMTUyMTM1WhcNMjAwOTI5MTUy
MTM1WjAUMRIwEAYDVQQDEwloYXJib3ItY2EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQDIaVSzd7YD7LR0ymnkhlvAOCMfDETc6Kd8NIJcgZVEZoeb7RqT
AEE6aSvw0IgZcU08mrmA2R2YplZj24FOy/Xj1qiMcRKY/YITkY4avNmJy2Wqpst0
QVu62VSACWpYg/455ZjcqEWs9Xu9c9+2dFW/zStxAK5gnQiUpUbTyjmNHVEYLRwu
U1vRPhbEJiIoorMeoAdplNra7I0tq5HWkyMWx1VDQHLEcVhFpJkTMtFqPzCkuSN4
ji1QapbO9jBCeD0n5xlhZ9GmZI2epzTXMacxL09VVD+od29SABdpmGIwMo9F559A
hMKOMcu1j7TJxJn2lROxIfm8afwo98YYsZBjAgMBAAGjQjBAMA4GA1UdDwEB/wQE
AwICpDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUw
AwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAxp+wqzb0iqaPJ3b0PcRob9SA+SjSrvdo
ZYxNZdKUxOIAGWrK4oidW2Nxy7YsqdH+O4pYGc1Mtmi0v+b8RJWNyiVTnD8ntSEO
AefkHEv9Z3E6iJCSV4/ileZFZKRaz/XgDAOtK3v54m0lJqXVdyrmPazHcRptNgt4
IDBvb6SiNSTOxBFt25tMMWMmOqbLNXIuJJYdOJsYg4mGsWjbflNMA9axM+IVWP+f
acWbqB4BD7Bj+zJY/gSX0AO36B4fQ713+nWQDy+ZQ67y3RVzcJeuohPo+hxgbHCA
RFTvMRj1bNPoUlCjU8iUGZiBJMuhaV5NSrSKYr1VcUiHas+/LQRznw==
-----END CERTIFICATE-----
11 changes: 6 additions & 5 deletions pkg/mock/registry_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package mock

import (
"github.com/docker/distribution"
"github.com/goharbor/harbor-scanner-clair/pkg/model/harbor"
"github.com/goharbor/harbor-scanner-clair/pkg/registry"
"github.com/stretchr/testify/mock"
)
Expand All @@ -22,12 +23,12 @@ func NewRegistryClient() *RegistryClient {
return &RegistryClient{}
}

func (f *RegistryClientFactory) Get(registryURL, authorization string) (registry.Client, error) {
args := f.Called(registryURL, authorization)
func (f *RegistryClientFactory) Get(req harbor.ScanRequest) (registry.Client, error) {
args := f.Called(req)
return args.Get(0).(registry.Client), args.Error(1)
}

func (c *RegistryClient) Manifest(repository, reference string) (distribution.Manifest, string, error) {
args := c.Called(repository, reference)
return args.Get(0).(distribution.Manifest), args.String(1), args.Error(2)
func (c *RegistryClient) GetManifest() (distribution.Manifest, error) {
args := c.Called()
return args.Get(0).(distribution.Manifest), args.Error(1)
}
65 changes: 33 additions & 32 deletions pkg/registry/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,81 +3,82 @@ package registry
import (
"crypto/tls"
"fmt"
"io/ioutil"
"net/http"
"strings"

"github.com/docker/distribution"
"github.com/docker/distribution/manifest/schema2"
log "github.com/sirupsen/logrus"
"github.com/goharbor/harbor-scanner-clair/pkg/etc"
"github.com/goharbor/harbor-scanner-clair/pkg/model/harbor"
"io/ioutil"
"net/http"
)

type ClientFactory interface {
Get(registryURL, authorization string) (Client, error)
Get(req harbor.ScanRequest) (Client, error)
}

type Client interface {
Manifest(repository, reference string) (distribution.Manifest, string, error)
GetManifest() (distribution.Manifest, error)
}

type client struct {
registryURL string
client *http.Client
authorization string
scanRequest harbor.ScanRequest
client *http.Client
}

type clientFactory struct {
tlsConfig etc.TLSConfig
}

func (cf *clientFactory) Get(registryURL, authorization string) (Client, error) {
func NewClientFactory(TLSConfig etc.TLSConfig) ClientFactory {
return &clientFactory{
tlsConfig: TLSConfig,
}
}

func (cf *clientFactory) Get(scanRequest harbor.ScanRequest) (Client, error) {
return &client{
registryURL: registryURL,
scanRequest: scanRequest,
client: &http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{
// FIXME Allow configuring custom or self-signed certs rather than skipping verification.
InsecureSkipVerify: true,
RootCAs: cf.tlsConfig.RootCAs,
InsecureSkipVerify: cf.tlsConfig.InsecureSkipVerify,
},
}},
authorization: authorization,
}, nil
}

func NewClientFactory() ClientFactory {
return &clientFactory{}
}

func (c *client) Manifest(repository, reference string) (distribution.Manifest, string, error) {
requestURL := fmt.Sprintf("%s/v2/%s/manifests/%s", c.registryURL, repository, reference)
log.Debugf("Fetch manifest URL: %s", requestURL)
req, err := http.NewRequest("GET", requestURL, nil)
func (c *client) GetManifest() (distribution.Manifest, error) {
req, err := http.NewRequest(http.MethodGet, c.manifestURL(), nil)
if err != nil {
return nil, "", err
return nil, err
}

req.Header.Add("Accept", schema2.MediaTypeManifest)
req.Header.Add("Authorization", c.authorization)
req.Header.Add("Authorization", c.scanRequest.Registry.Authorization)

resp, err := c.client.Do(req)
if err != nil {
return nil, "", err
return nil, err
}
log.Debugf("Response status: %s", resp.Status)
log.Debugf("Response headers: %v", resp.Header)

defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, "", err
return nil, err
}

if resp.StatusCode != http.StatusOK {
return nil, "", fmt.Errorf("HTTP not ok: %v", resp.Status)
return nil, fmt.Errorf("fetching manifest with status %q: %s", resp.Status, string(b))
}

manifest, _, err := distribution.UnmarshalManifest(schema2.MediaTypeManifest, b)
if err != nil {
return nil, "", fmt.Errorf("unmarshaling manifest: %v", err)
return nil, fmt.Errorf("unmarshaling manifest: %v", err)
}
return manifest, nil
}

return manifest, strings.TrimPrefix(c.authorization, "Bearer "), nil
func (c *client) manifestURL() string {
return fmt.Sprintf("%s/v2/%s/manifests/%s", c.scanRequest.Registry.URL,
c.scanRequest.Artifact.Repository,
c.scanRequest.Artifact.Digest)
}
9 changes: 5 additions & 4 deletions pkg/scanner/clair/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"github.com/goharbor/harbor-scanner-clair/pkg/etc"
"github.com/goharbor/harbor-scanner-clair/pkg/model/clair"
"io/ioutil"
"net/http"
Expand All @@ -24,13 +25,13 @@ type client struct {
}

// NewClient constructs a new client for Clair REST API pointing to the specified endpoint URL.
func NewClient(endpointURL string) Client {
func NewClient(tlsConfig etc.TLSConfig, cfg etc.ClairConfig) Client {
return &client{
endpointURL: strings.TrimSuffix(endpointURL, "/"),
endpointURL: strings.TrimSuffix(cfg.URL, "/"),
client: &http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{
// FIXME Allow configuring custom or self-signed certs rather that skipping verification.
InsecureSkipVerify: true,
InsecureSkipVerify: tlsConfig.InsecureSkipVerify,
RootCAs: tlsConfig.RootCAs,
},
}},
}
Expand Down
Loading