From 1ac3f7434b128be53e960f773016b247b9b3e607 Mon Sep 17 00:00:00 2001 From: Chris Grindstaff Date: Wed, 26 Jul 2023 11:14:57 -0400 Subject: [PATCH 1/3] feat: Harvest should fetch certificates via a script --- cmd/collectors/storagegrid/rest/client.go | 56 +--- cmd/poller/poller.go | 31 +- cmd/tools/rest/client.go | 76 +---- harvest.cue | 6 + pkg/api/ontapi/zapi/client.go | 77 +---- pkg/auth/auth.go | 254 +++++++++++++-- pkg/auth/auth_test.go | 376 +++++++++++++++------- pkg/auth/testdata/get_cert | 12 + pkg/auth/testdata/get_cert2 | 13 + pkg/conf/conf.go | 10 + 10 files changed, 536 insertions(+), 375 deletions(-) create mode 100755 pkg/auth/testdata/get_cert create mode 100755 pkg/auth/testdata/get_cert2 diff --git a/cmd/collectors/storagegrid/rest/client.go b/cmd/collectors/storagegrid/rest/client.go index 0d0d1bf90..b725ae7b4 100644 --- a/cmd/collectors/storagegrid/rest/client.go +++ b/cmd/collectors/storagegrid/rest/client.go @@ -2,7 +2,6 @@ package rest import ( "bytes" - "crypto/tls" "encoding/json" "errors" "fmt" @@ -76,14 +75,12 @@ func NewClient(pollerName string, clientTimeout string, c *auth.Credentials) (*C func New(poller *conf.Poller, timeout time.Duration, c *auth.Credentials) (*Client, error) { var ( - client Client - httpclient *http.Client - transport *http.Transport - cert tls.Certificate - addr string - href string - useInsecureTLS bool - err error + client Client + httpclient *http.Client + transport *http.Transport + addr string + href string + err error ) client = Client{ @@ -100,49 +97,10 @@ func New(poller *conf.Poller, timeout time.Duration, c *auth.Credentials) (*Clie client.baseURL = href client.Timeout = timeout - // by default, enforce secure TLS, if not requested otherwise by user - if x := poller.UseInsecureTLS; x != nil { - useInsecureTLS = *poller.UseInsecureTLS - } else { - useInsecureTLS = false - } - - pollerAuth, err := c.GetPollerAuth() + transport, err = c.Transport(nil) if err != nil { return nil, err } - if pollerAuth.IsCert { - certPath := poller.SslCert - keyPath := poller.SslKey - if certPath == "" { - return nil, errs.New(errs.ErrMissingParam, "ssl_cert") - } else if keyPath == "" { - return nil, errs.New(errs.ErrMissingParam, "ssl_key") - } else if cert, err = tls.LoadX509KeyPair(certPath, keyPath); err != nil { - return nil, err - } - - transport = &http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSClientConfig: &tls.Config{ - Certificates: []tls.Certificate{cert}, - InsecureSkipVerify: useInsecureTLS}, //nolint:gosec - } - } else { - username := pollerAuth.Username - password := pollerAuth.Password - client.username = username - if username == "" { - return nil, errs.New(errs.ErrMissingParam, "username") - } else if password == "" { - return nil, errs.New(errs.ErrMissingParam, "password") - } - - transport = &http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSClientConfig: &tls.Config{InsecureSkipVerify: useInsecureTLS}, //nolint:gosec - } - } httpclient = &http.Client{Transport: transport, Timeout: timeout} client.client = httpclient diff --git a/cmd/poller/poller.go b/cmd/poller/poller.go index bcab5d9a8..83967dfa5 100644 --- a/cmd/poller/poller.go +++ b/cmd/poller/poller.go @@ -54,7 +54,6 @@ import ( "github.com/netapp/harvest/v2/pkg/requests" "github.com/netapp/harvest/v2/pkg/tree/node" "github.com/netapp/harvest/v2/pkg/util" - "github.com/rs/zerolog/log" "github.com/spf13/cobra" "gopkg.in/yaml.v3" "io" @@ -64,7 +63,6 @@ import ( "os" "os/exec" "os/signal" - "path" "regexp" "runtime" "strconv" @@ -243,33 +241,6 @@ func (p *Poller) Init() error { // create a shared auth service that all collectors will use p.auth = auth.NewCredentials(p.params, logger) - pollerAuth, err := p.auth.GetPollerAuth() - if err != nil { - return err - } - - // check optional parameter auth_style - // if certificates are missing use default paths - if pollerAuth.IsCert { - if p.params.SslCert == "" { - fp := path.Join(p.options.HomePath, "cert/", p.options.Hostname+".pem") - p.params.SslCert = fp - logger.Debug().Msgf("using default [ssl_cert] path: [%s]", fp) - if _, err = os.Stat(fp); err != nil { - logger.Error().Stack().Err(err).Msgf("ssl_cert") - return errs.New(errs.ErrMissingParam, "ssl_cert: "+err.Error()) - } - } - if p.params.SslKey == "" { - fp := path.Join(p.options.HomePath, "cert/", p.options.Hostname+".key") - p.params.SslKey = fp - logger.Debug().Msgf("using default [ssl_key] path: [%s]", fp) - if _, err = os.Stat(fp); err != nil { - logger.Error().Stack().Err(err).Msgf("ssl_key") - return errs.New(errs.ErrMissingParam, "ssl_key: "+err.Error()) - } - } - } // initialize our metadata, the metadata will host status of our // collectors and exporters, as well as ping stats to target host @@ -1167,7 +1138,7 @@ func (p *Poller) negotiateAPI(c conf.Collector, checkZAPIs func() error) conf.Co Templates: c.Templates, } } - log.Error().Err(err).Str("collector", c.Name).Msg("Failed to negotiateAPI") + logger.Error().Err(err).Str("collector", c.Name).Msg("Failed to negotiateAPI") } return c diff --git a/cmd/tools/rest/client.go b/cmd/tools/rest/client.go index 54f166a14..91b4abbe1 100644 --- a/cmd/tools/rest/client.go +++ b/cmd/tools/rest/client.go @@ -4,8 +4,6 @@ package rest import ( "bytes" - "crypto/tls" - "crypto/x509" "errors" "fmt" "github.com/netapp/harvest/v2/pkg/auth" @@ -54,14 +52,12 @@ type Cluster struct { func New(poller *conf.Poller, timeout time.Duration, auth *auth.Credentials) (*Client, error) { var ( - client Client - httpclient *http.Client - transport *http.Transport - cert tls.Certificate - addr string - url string - useInsecureTLS bool - err error + client Client + httpclient *http.Client + transport *http.Transport + addr string + url string + err error ) client = Client{ @@ -81,69 +77,11 @@ func New(poller *conf.Poller, timeout time.Duration, auth *auth.Credentials) (*C client.baseURL = url client.Timeout = timeout - // by default, enforce secure TLS, if not requested otherwise by user - if x := poller.UseInsecureTLS; x != nil { - useInsecureTLS = *poller.UseInsecureTLS - } else { - useInsecureTLS = false - } - - pollerAuth, err := auth.GetPollerAuth() + transport, err = auth.Transport(nil) if err != nil { return nil, err } - if pollerAuth.IsCert { - sslCertPath := poller.SslCert - keyPath := poller.SslKey - caCertPath := poller.CaCertPath - - if sslCertPath == "" { - return nil, errs.New(errs.ErrMissingParam, "ssl_cert") - } else if keyPath == "" { - return nil, errs.New(errs.ErrMissingParam, "ssl_key") - } else if cert, err = tls.LoadX509KeyPair(sslCertPath, keyPath); err != nil { - return nil, err - } - - // Create a CA certificate pool and add certificate if specified - caCertPool := x509.NewCertPool() - if caCertPath != "" { - caCert, err := os.ReadFile(caCertPath) - if err != nil { - client.Logger.Error().Err(err).Str("cacert", caCertPath).Msg("Failed to read ca cert") - // continue - } - if caCert != nil { - pem := caCertPool.AppendCertsFromPEM(caCert) - if !pem { - client.Logger.Error().Err(err).Str("cacert", caCertPath).Msg("Failed to append ca cert") - // continue - } - } - } - - transport = &http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSClientConfig: &tls.Config{ - RootCAs: caCertPool, - Certificates: []tls.Certificate{cert}, - InsecureSkipVerify: useInsecureTLS}, //nolint:gosec - } - } else { - if pollerAuth.Username == "" { - return nil, errs.New(errs.ErrMissingParam, "username") - } else if pollerAuth.Password == "" { - return nil, errs.New(errs.ErrMissingParam, "password") - } - client.username = pollerAuth.Username - - transport = &http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSClientConfig: &tls.Config{InsecureSkipVerify: useInsecureTLS}, //nolint:gosec - } - } - transport.DialContext = (&net.Dialer{Timeout: DefaultDialerTimeout}).DialContext httpclient = &http.Client{Transport: transport, Timeout: timeout} client.client = httpclient diff --git a/harvest.cue b/harvest.cue index d678de2e8..ae33e119d 100644 --- a/harvest.cue +++ b/harvest.cue @@ -49,6 +49,11 @@ label: [string]: string url?: string } +#CertificateScript: { + path: string + timeout?: string +} + #CredentialsScript: { path: string schedule?: string @@ -68,6 +73,7 @@ Pollers: [Name=_]: #Poller collectors?: [...#CollectorDef] | [...string] credentials_file?: string credentials_script?: #CredentialsScript + certificate_script?: #CertificateScript datacenter?: string exporters: [...string] is_kfs?: bool diff --git a/pkg/api/ontapi/zapi/client.go b/pkg/api/ontapi/zapi/client.go index 13b5f1257..574568336 100644 --- a/pkg/api/ontapi/zapi/client.go +++ b/pkg/api/ontapi/zapi/client.go @@ -7,7 +7,6 @@ package zapi import ( "bytes" "crypto/tls" - "crypto/x509" "errors" "fmt" "github.com/netapp/harvest/v2/pkg/auth" @@ -19,7 +18,6 @@ import ( "github.com/netapp/harvest/v2/pkg/tree/node" "io" "net/http" - "os" "strconv" "strings" "time" @@ -45,15 +43,13 @@ type Client struct { func New(poller *conf.Poller, c *auth.Credentials) (*Client, error) { var ( - client Client - httpclient *http.Client - request *http.Request - transport *http.Transport - cert tls.Certificate - timeout time.Duration - url, addr string - useInsecureTLS bool - err error + client Client + httpclient *http.Client + request *http.Request + transport *http.Transport + timeout time.Duration + url, addr string + err error ) client = Client{ @@ -98,68 +94,11 @@ func New(poller *conf.Poller, c *auth.Credentials) (*Client, error) { request.Header.Set("Content-type", "text/xml") request.Header.Set("Charset", "utf-8") - // by default, enforce secure TLS, if not requested otherwise by user - useInsecureTLS = false - if poller.UseInsecureTLS != nil { - useInsecureTLS = *poller.UseInsecureTLS - } - - pollerAuth, err := c.GetPollerAuth() + transport, err = c.Transport(request) if err != nil { return nil, err } - if pollerAuth.IsCert { - sslCertPath := poller.SslCert - keyPath := poller.SslKey - caCertPath := poller.CaCertPath - - if sslCertPath == "" { - return nil, errs.New(errs.ErrMissingParam, "ssl_cert") - } else if keyPath == "" { - return nil, errs.New(errs.ErrMissingParam, "ssl_key") - } else if cert, err = tls.LoadX509KeyPair(sslCertPath, keyPath); err != nil { - return nil, err - } - - // Create a CA certificate pool and add certificate if specified - caCertPool := x509.NewCertPool() - if caCertPath != "" { - caCert, err := os.ReadFile(caCertPath) - if err != nil { - client.Logger.Error().Err(err).Str("cacert", caCertPath).Msg("Failed to read ca cert") - // continue - } - if caCert != nil { - pem := caCertPool.AppendCertsFromPEM(caCert) - if !pem { - client.Logger.Error().Err(err).Str("cacert", caCertPath).Msg("Failed to append ca cert") - // continue - } - } - } - - transport = &http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSClientConfig: &tls.Config{ - RootCAs: caCertPool, - Certificates: []tls.Certificate{cert}, - InsecureSkipVerify: useInsecureTLS, //nolint:gosec - }, - } - } else { - password := pollerAuth.Password - if pollerAuth.Username == "" { - return nil, errs.New(errs.ErrMissingParam, "username") - } else if password == "" { - return nil, errs.New(errs.ErrMissingParam, "password") - } - request.SetBasicAuth(pollerAuth.Username, password) - transport = &http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSClientConfig: &tls.Config{InsecureSkipVerify: useInsecureTLS}, //nolint:gosec - } - } if poller.TLSMinVersion != "" { tlsVersion := client.tlsVersion(poller.TLSMinVersion) if tlsVersion != 0 { diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 020f29e56..ae3e8801b 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -3,10 +3,19 @@ package auth import ( "bytes" "context" + "crypto/tls" + "crypto/x509" "dario.cat/mergo" + "encoding/pem" + "fmt" + "github.com/netapp/harvest/v2/cmd/poller/options" "github.com/netapp/harvest/v2/pkg/conf" + "github.com/netapp/harvest/v2/pkg/errs" "github.com/netapp/harvest/v2/pkg/logging" + "net/http" + "os" "os/exec" + "path" "strings" "sync" "syscall" @@ -16,6 +25,8 @@ import ( const ( defaultSchedule = "24h" defaultTimeout = "10s" + certType = "CERTIFICATE" + keyType = "PRIVATE KEY" ) func NewCredentials(p *conf.Poller, logger *logging.Logger) *Credentials { @@ -58,26 +69,49 @@ func (c *Credentials) Expire() { c.nextUpdate = time.Time{} } -func (c *Credentials) password(poller *conf.Poller) string { +func (c *Credentials) certs(poller *conf.Poller) (string, error) { + if poller.CertificateScript.Path == "" { + return "", nil + } + c.authMu.Lock() + defer c.authMu.Unlock() + return c.fetchCerts(poller) +} + +func (c *Credentials) password(poller *conf.Poller) (string, error) { if poller.CredentialsScript.Path == "" { - return poller.Password + return poller.Password, nil } c.authMu.Lock() defer c.authMu.Unlock() if time.Now().After(c.nextUpdate) { - c.cachedPassword = c.fetchPassword(poller) + var err error + c.cachedPassword, err = c.fetchPassword(poller) + if err != nil { + return "", err + } c.setNextUpdate() } - return c.cachedPassword + return c.cachedPassword, nil +} + +func (c *Credentials) fetchPassword(p *conf.Poller) (string, error) { + return c.execScript(p.CredentialsScript.Path, "credential", p.CredentialsScript.Timeout, func(ctx context.Context, path string) *exec.Cmd { + return exec.CommandContext(ctx, path, p.Addr, p.Username) // #nosec + }) } -func (c *Credentials) fetchPassword(p *conf.Poller) string { - path, err := exec.LookPath(p.CredentialsScript.Path) +func (c *Credentials) fetchCerts(p *conf.Poller) (string, error) { + return c.execScript(p.CertificateScript.Path, "certificate", p.CertificateScript.Timeout, func(ctx context.Context, path string) *exec.Cmd { + return exec.CommandContext(ctx, path, p.Addr) // #nosec + }) +} + +func (c *Credentials) execScript(cmdPath string, kind string, timeout string, e func(ctx context.Context, path string) *exec.Cmd) (string, error) { + lookPath, err := exec.LookPath(cmdPath) if err != nil { - c.logger.Error().Err(err).Str("path", p.CredentialsScript.Path).Msg("Credentials script lookup failed") - return "" + return "", fmt.Errorf("script lookup failed kind=%s err=%w", kind, err) } - timeout := p.CredentialsScript.Timeout if timeout == "" { timeout = defaultTimeout } @@ -91,7 +125,7 @@ func (c *Credentials) fetchPassword(p *conf.Poller) string { } ctx, cancelFunc := context.WithTimeout(context.Background(), duration) defer cancelFunc() - cmd := exec.CommandContext(ctx, path, p.Addr, p.Username) + cmd := e(ctx, lookPath) // Create process group - so we can kill any forked processes cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} @@ -108,24 +142,26 @@ func (c *Credentials) fetchPassword(p *conf.Poller) string { }() if err != nil { c.logger.Error().Err(err). - Str("script", path). + Str("script", lookPath). Str("timeout", duration.String()). Str("stderr", stderr.String()). Str("stdout", stdout.String()). - Msg("Failed to start credentials script") - return "" + Str("kind", kind). + Msg("Failed to start script") + return "", fmt.Errorf("script start failed script=%s kind=%s err=%w", lookPath, kind, err) } err = cmd.Wait() if err != nil { c.logger.Error().Err(err). - Str("script", path). + Str("script", lookPath). Str("timeout", duration.String()). Str("stderr", stderr.String()). Str("stdout", stdout.String()). - Msg("Failed to execute credentials script") - return "" + Str("kind", kind). + Msg("Failed to execute script") + return "", fmt.Errorf("script execute failed script=%s kind=%s err=%w", lookPath, kind, err) } - return strings.TrimSpace(stdout.String()) + return strings.TrimSpace(stdout.String()), nil } func (c *Credentials) setNextUpdate() { @@ -148,11 +184,49 @@ func (c *Credentials) setNextUpdate() { } type PollerAuth struct { - Username string - Password string - IsCert bool - HasCredentialScript bool - Schedule string + Username string + Password string + IsCert bool + HasCredentialScript bool + HasCertificateScript bool + Schedule string + PemCert []byte + PemKey []byte + CertPath string + KeyPath string + CaCertPath string + insecureTLS bool +} + +func (a PollerAuth) Certificate() (tls.Certificate, error) { + if a.HasCertificateScript { + return tls.X509KeyPair(a.PemCert, a.PemKey) + } + if a.CertPath == "" { + return tls.Certificate{}, errs.New(errs.ErrMissingParam, "ssl_cert") + } + if a.KeyPath == "" { + return tls.Certificate{}, errs.New(errs.ErrMissingParam, "ssl_key") + } + return tls.LoadX509KeyPair(a.CertPath, a.KeyPath) +} + +func (a PollerAuth) NewCertPool() (*x509.CertPool, error) { + // Create a CA certificate pool and add certificate if specified + caCertPool := x509.NewCertPool() + if a.CaCertPath != "" { + caCert, err := os.ReadFile(a.CaCertPath) + if err != nil { + return caCertPool, err + } + if caCert != nil { + ok := caCertPool.AppendCertsFromPEM(caCert) + if !ok { + return caCertPool, fmt.Errorf("failed to append ca cert path=%s", a.CaCertPath) + } + } + } + return caCertPool, nil } func (c *Credentials) GetPollerAuth() (PollerAuth, error) { @@ -181,29 +255,33 @@ func (c *Credentials) GetPollerAuth() (PollerAuth, error) { if err != nil { return PollerAuth{}, err } - if auth.IsCert { - return auth, nil - } - if auth.Username != "" { - defaultAuth.Username = auth.Username - } _ = mergo.Merge(&auth, defaultAuth) return auth, nil } func getPollerAuth(c *Credentials, poller *conf.Poller) (PollerAuth, error) { + // by default, enforce secure TLS + insecureTLS := false + if poller.UseInsecureTLS != nil { + insecureTLS = *poller.UseInsecureTLS + } if poller.AuthStyle == conf.CertificateAuth { - return PollerAuth{IsCert: true}, nil + return handCertificateAuth(c, poller, insecureTLS) } if poller.Password != "" { - return PollerAuth{Username: poller.Username, Password: poller.Password}, nil + return PollerAuth{Username: poller.Username, Password: poller.Password, insecureTLS: insecureTLS}, nil } if poller.CredentialsScript.Path != "" { + pass, err := c.password(poller) + if err != nil { + return PollerAuth{}, err + } return PollerAuth{ Username: poller.Username, - Password: c.password(poller), + Password: pass, HasCredentialScript: true, Schedule: poller.CredentialsScript.Schedule, + insecureTLS: insecureTLS, }, nil } if poller.CredentialsFile != "" { @@ -211,7 +289,117 @@ func getPollerAuth(c *Credentials, poller *conf.Poller) (PollerAuth, error) { if err != nil { return PollerAuth{}, err } - return PollerAuth{Username: poller.Username, Password: poller.Password}, nil + return PollerAuth{Username: poller.Username, Password: poller.Password, insecureTLS: insecureTLS}, nil + } + return PollerAuth{Username: poller.Username, insecureTLS: insecureTLS}, nil +} + +func handCertificateAuth(c *Credentials, poller *conf.Poller, insecureTLS bool) (PollerAuth, error) { + if poller.CertificateScript.Path != "" { + certBlob, err := c.certs(poller) + if err != nil { + return PollerAuth{}, err + } + cert, key, err := extractCertAndKey(certBlob) + if err != nil { + return PollerAuth{}, err + } + return PollerAuth{ + IsCert: true, + HasCertificateScript: true, + PemCert: cert, + PemKey: key, + insecureTLS: insecureTLS, + }, nil + } + + var pathPrefix string + certPath := poller.SslCert + keyPath := poller.SslKey + + if certPath == "" { + o := &options.Options{} + options.SetPathsAndHostname(o) + pathPrefix = path.Join(o.HomePath, "cert/", o.Hostname) + certPath = pathPrefix + ".pem" + } + if keyPath == "" { + keyPath = pathPrefix + ".key" + } + return PollerAuth{ + IsCert: true, + CertPath: certPath, + KeyPath: keyPath, + CaCertPath: poller.CaCertPath, + insecureTLS: insecureTLS, + }, nil +} + +func extractCertAndKey(blob string) ([]byte, []byte, error) { + block1, rest := pem.Decode([]byte(blob)) + block2, _ := pem.Decode(rest) + + if block1 == nil { + return nil, nil, fmt.Errorf("PEM block1 is nil") + } + if block2 == nil { + return nil, nil, fmt.Errorf("PEM block2 is nil") + } + + if block1.Type == certType && block2.Type == keyType { + return bytes.TrimSpace(pem.EncodeToMemory(block1)), bytes.TrimSpace(pem.EncodeToMemory(block2)), nil + } + if block1.Type == keyType && block2.Type == certType { + return bytes.TrimSpace(pem.EncodeToMemory(block2)), bytes.TrimSpace(pem.EncodeToMemory(block1)), nil + } + + return nil, nil, fmt.Errorf("unexpected PEM block1Type=%s block2Type=%s", block1.Type, block2.Type) +} + +func (c *Credentials) Transport(request *http.Request) (*http.Transport, error) { + var ( + cert tls.Certificate + transport *http.Transport + ) + + pollerAuth, err := c.GetPollerAuth() + if err != nil { + return nil, err + } + + if pollerAuth.IsCert { + cert, err = pollerAuth.Certificate() + if err != nil { + return nil, err + } + caCertPool, err := pollerAuth.NewCertPool() + if err != nil { + return nil, err + } + + transport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{ + RootCAs: caCertPool, + Certificates: []tls.Certificate{cert}, + InsecureSkipVerify: pollerAuth.insecureTLS, //nolint:gosec + }, + } + } else { + password := pollerAuth.Password + if pollerAuth.Username == "" { + return nil, errs.New(errs.ErrMissingParam, "username") + } else if password == "" { + return nil, errs.New(errs.ErrMissingParam, "password") + } + + if request != nil { + request.SetBasicAuth(pollerAuth.Username, password) + } + transport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{InsecureSkipVerify: pollerAuth.insecureTLS}, //nolint:gosec + } } - return PollerAuth{Username: poller.Username}, nil + return transport, err } diff --git a/pkg/auth/auth_test.go b/pkg/auth/auth_test.go index ff88bc09b..0f0d330cc 100644 --- a/pkg/auth/auth_test.go +++ b/pkg/auth/auth_test.go @@ -3,7 +3,8 @@ package auth import ( "github.com/netapp/harvest/v2/pkg/conf" "github.com/netapp/harvest/v2/pkg/logging" - "strings" + "os" + "reflect" "testing" ) @@ -25,10 +26,10 @@ func TestCredentials_GetPollerAuth(t *testing.T) { defaultDefined: false, yaml: ` Pollers: - test: - addr: a.b.c - username: username - credentials_file: testdata/secrets.yaml`, + test: + addr: a.b.c + username: username + credentials_file: testdata/secrets.yaml`, }, { @@ -38,17 +39,17 @@ Pollers: defaultDefined: true, yaml: ` Defaults: - auth_style: certificate_auth - credentials_file: secrets/openlab - username: me - password: pass - credentials_script: - path: ../get_pass + auth_style: certificate_auth + credentials_file: secrets/openlab + username: me + password: pass + credentials_script: + path: ../get_pass Pollers: - test: - addr: a.b.c - username: username - credentials_file: testdata/secrets.yaml`, + test: + addr: a.b.c + username: username + credentials_file: testdata/secrets.yaml`, }, { @@ -58,12 +59,12 @@ Pollers: defaultDefined: true, yaml: ` Defaults: - auth_style: certificate_auth - credentials_file: secrets/openlab + auth_style: certificate_auth + credentials_file: secrets/openlab Pollers: - test: - addr: a.b.c - credentials_file: testdata/secrets.yaml`, + test: + addr: a.b.c + credentials_file: testdata/secrets.yaml`, }, { @@ -73,11 +74,11 @@ Pollers: defaultDefined: true, yaml: ` Defaults: - credentials_file: testdata/secrets.yaml + credentials_file: testdata/secrets.yaml Pollers: - test: - addr: a.b.c - password: moon`, + test: + addr: a.b.c + password: moon`, }, { @@ -87,31 +88,33 @@ Pollers: defaultDefined: true, yaml: ` Defaults: - auth_style: certificate_auth - credentials_file: secrets/openlab + auth_style: certificate_auth + credentials_file: secrets/openlab Pollers: - test2: - addr: a.b.c - credentials_file: testdata/secrets.yaml`, + test2: + addr: a.b.c + credentials_file: testdata/secrets.yaml`, }, { name: "default cert_auth", pollerName: "test", - want: PollerAuth{Username: "username", Password: "", IsCert: true}, + want: PollerAuth{Username: "username", IsCert: true, CertPath: "my_cert", KeyPath: "my_key"}, defaultDefined: true, yaml: ` Defaults: - auth_style: certificate_auth - credentials_file: secrets/openlab - username: me - password: pass - credentials_script: - path: ../get_pass + auth_style: certificate_auth + ssl_cert: my_cert + ssl_key: my_key + credentials_file: secrets/openlab + username: me + password: pass + credentials_script: + path: ../get_pass Pollers: - test: - addr: a.b.c - username: username`, + test: + addr: a.b.c + username: username`, }, { @@ -121,17 +124,17 @@ Pollers: defaultDefined: true, yaml: ` Defaults: - auth_style: certificate_auth - credentials_file: secrets/openlab - username: me - password: pass - credentials_script: - path: ../get_pass + auth_style: certificate_auth + credentials_file: secrets/openlab + username: me + password: pass + credentials_script: + path: ../get_pass Pollers: - test: - addr: a.b.c - username: username - password: pass`, + test: + addr: a.b.c + username: username + password: pass`, }, { @@ -141,16 +144,16 @@ Pollers: defaultDefined: true, yaml: ` Defaults: - auth_style: certificate_auth - credentials_file: secrets/openlab - username: me - password: pass - credentials_script: - path: ../get_pass + auth_style: certificate_auth + credentials_file: secrets/openlab + username: me + password: pass + credentials_script: + path: ../get_pass Pollers: - test: - addr: a.b.c - password: pass2`, + test: + addr: a.b.c + password: pass2`, }, { @@ -165,13 +168,13 @@ Pollers: defaultDefined: true, yaml: ` Defaults: - username: me - credentials_script: - path: testdata/get_pass + username: me + credentials_script: + path: testdata/get_pass Pollers: - test: - addr: a.b.c - username: username`, + test: + addr: a.b.c + username: username`, }, { @@ -181,12 +184,12 @@ Pollers: defaultDefined: true, yaml: ` Defaults: - username: me - credentials_script: - path: testdata/get_pass + username: me + credentials_script: + path: testdata/get_pass Pollers: - test: - addr: a.b.c`, + test: + addr: a.b.c`, }, { @@ -195,11 +198,11 @@ Pollers: want: PollerAuth{Username: "username", Password: "addr=a.b.c user=username", HasCredentialScript: true}, yaml: ` Pollers: - test: - addr: a.b.c - credentials_script: - path: testdata/get_pass - username: username`, + test: + addr: a.b.c + credentials_script: + path: testdata/get_pass + username: username`, }, { @@ -208,8 +211,8 @@ Pollers: want: PollerAuth{Username: "", Password: "", IsCert: false}, yaml: ` Pollers: - test: - addr: a.b.c`, + test: + addr: a.b.c`, }, { @@ -218,24 +221,25 @@ Pollers: want: PollerAuth{Username: "default-user", Password: "default-pass", IsCert: false}, yaml: ` Pollers: - missing: - addr: a.b.c - credentials_file: testdata/secrets.yaml`, + missing: + addr: a.b.c + credentials_file: testdata/secrets.yaml`, }, { name: "with cred", pollerName: "test", - want: PollerAuth{Username: "", Password: "", IsCert: true}, + want: PollerAuth{IsCert: true, CertPath: "my_cert", KeyPath: "my_key"}, yaml: ` Defaults: - use_insecure_tls: true - prefer_zapi: true + use_insecure_tls: true + prefer_zapi: true Pollers: - test: - addr: a.b.c - auth_style: certificate_auth -`, + test: + addr: a.b.c + auth_style: certificate_auth + ssl_cert: my_cert + ssl_key: my_key`, }, { @@ -244,16 +248,16 @@ Pollers: want: PollerAuth{Username: "bat", Password: "addr=a.b.c user=bat", HasCredentialScript: true}, yaml: ` Defaults: - use_insecure_tls: true - prefer_zapi: true - credentials_script: - path: testdata/get_pass2 + use_insecure_tls: true + prefer_zapi: true + credentials_script: + path: testdata/get_pass2 Pollers: - test: - addr: a.b.c - username: bat - credentials_script: - path: testdata/get_pass + test: + addr: a.b.c + username: bat + credentials_script: + path: testdata/get_pass `, }, @@ -264,18 +268,18 @@ Pollers: wantSchedule: "15m", yaml: ` Defaults: - use_insecure_tls: true - prefer_zapi: true - credentials_script: - path: testdata/get_pass - schedule: 45m + use_insecure_tls: true + prefer_zapi: true + credentials_script: + path: testdata/get_pass + schedule: 45m Pollers: - test: - addr: a.b.c - username: flo - credentials_script: - path: testdata/get_pass - schedule: 15m + test: + addr: a.b.c + username: flo + credentials_script: + path: testdata/get_pass + schedule: 15m `, }, @@ -286,16 +290,16 @@ Pollers: wantSchedule: "42m", yaml: ` Defaults: - use_insecure_tls: true - prefer_zapi: true - credentials_script: - schedule: 42m + use_insecure_tls: true + prefer_zapi: true + credentials_script: + schedule: 42m Pollers: - test: - addr: a.b.c - username: flo - credentials_script: - path: testdata/get_pass + test: + addr: a.b.c + username: flo + credentials_script: + path: testdata/get_pass `, }, @@ -306,21 +310,136 @@ Pollers: wantSchedule: "42m", yaml: ` Pollers: - test: - addr: a.b.c - username: flo - password: abc def + test: + addr: a.b.c + username: flo + password: abc def +`, + }, + + { + name: "certificate_script in poller", + pollerName: "test", + want: PollerAuth{ + IsCert: true, PemCert: []byte(`-----BEGIN CERTIFICATE----- +SSA8MyBIYXJ2ZXN0 +-----END CERTIFICATE-----`), PemKey: []byte(`-----BEGIN PRIVATE KEY----- +c3VwZXIgc2VjcmV0 +-----END PRIVATE KEY-----`), + }, + yaml: ` +Pollers: + test: + auth_style: certificate_auth + addr: a.b.c + certificate_script: + path: testdata/get_cert +`, + }, + + { + name: "certificate_script in defaults", + pollerName: "test", + want: PollerAuth{ + IsCert: true, PemCert: []byte(`-----BEGIN CERTIFICATE----- +SSA8MyBIYXJ2ZXN0 +-----END CERTIFICATE-----`), PemKey: []byte(`-----BEGIN PRIVATE KEY----- +c3VwZXIgc2VjcmV0 +-----END PRIVATE KEY-----`), + }, + yaml: ` +Defaults: + certificate_script: + path: testdata/get_cert +Pollers: + test: + auth_style: certificate_auth + addr: a.b.c +`, + }, + + { + name: "certificate_script in both", + pollerName: "test", + want: PollerAuth{ + IsCert: true, PemCert: []byte(`-----BEGIN CERTIFICATE----- +SSA8MyBIYXJ2ZXN0 +-----END CERTIFICATE-----`), PemKey: []byte(`-----BEGIN PRIVATE KEY----- +c3VwZXIgc2VjcmV0 +-----END PRIVATE KEY-----`), + }, + yaml: ` +Defaults: + certificate_script: + path: testdata/get_cert2 +Pollers: + test: + auth_style: certificate_auth + addr: a.b.c + certificate_script: + path: testdata/get_cert +`, + }, + + { + name: "ssl_cert and ssl_key defaults", + pollerName: "test", + want: PollerAuth{IsCert: true, CertPath: "ssl_cert", KeyPath: "ssl_key"}, + yaml: ` +Defaults: + ssl_cert: ssl_cert + ssl_key: ssl_key +Pollers: + test: + auth_style: certificate_auth + addr: a.b.c +`, + }, + + { + name: "certificate_auth with ssl_cert in both", + pollerName: "test", + want: PollerAuth{IsCert: true, CertPath: "ssl_cert", KeyPath: "ssl_key"}, + yaml: ` +Defaults: + ssl_cert: default_ssl_cert + ssl_key: default_ssl_cert +Pollers: + test: + auth_style: certificate_auth + addr: a.b.c + ssl_cert: ssl_cert + ssl_key: ssl_key +`, + }, + + { + name: "optional ssl_cert and ssl_key", + pollerName: "test", + want: PollerAuth{IsCert: true, CertPath: "cert/cgrindst-mac-0.pem", KeyPath: "cert/cgrindst-mac-0.key"}, + yaml: ` +Pollers: + test: + auth_style: certificate_auth + addr: a.b.c `, }, } + hostname, err := os.Hostname() + if err != nil { + t.Errorf("failed to get hostname err: %v", err) + } + hostCertPath := "cert/" + hostname + ".pem" + hostKeyPath := "cert/" + hostname + ".key" + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { conf.Config.Defaults = nil if tt.defaultDefined { conf.Config.Defaults = &conf.Poller{} } - err := conf.DecodeConfig(toYaml(tt.yaml)) + err := conf.DecodeConfig([]byte(tt.yaml)) if err != nil { t.Errorf("expected no error got %+v", err) return @@ -352,11 +471,18 @@ Pollers: tt.want.HasCredentialScript, ) } + if !reflect.DeepEqual(tt.want.PemCert, got.PemCert) { + t.Errorf("got PemCert=[%s], want PemCert=[%s]", got.PemCert, tt.want.PemCert) + } + if !reflect.DeepEqual(tt.want.PemKey, got.PemKey) { + t.Errorf("got PemKey=[%s], want PemKey=[%s]", got.PemKey, tt.want.PemKey) + } + if tt.want.CertPath != got.CertPath && got.CertPath != hostCertPath { + t.Errorf("got CertPath=[%s], want CertPath=[%s]", got.CertPath, tt.want.CertPath) + } + if tt.want.KeyPath != got.KeyPath && got.KeyPath != hostKeyPath { + t.Errorf("got KeyPath=[%s], want KeyPath=[%s]", got.KeyPath, tt.want.KeyPath) + } }) } } - -func toYaml(s string) []byte { - all := strings.ReplaceAll(s, "\t", " ") - return []byte(all) -} diff --git a/pkg/auth/testdata/get_cert b/pkg/auth/testdata/get_cert new file mode 100755 index 000000000..163fa4f7e --- /dev/null +++ b/pkg/auth/testdata/get_cert @@ -0,0 +1,12 @@ +#!/bin/bash +# Used by pkg/auth/auth_test.go +# echo -n 'I <3 Harvest' | base64 +# SSA8MyBIYXJ2ZXN0 +# echo -n 'super secret' | base64 +# c3VwZXIgc2VjcmV0 +echo "-----BEGIN CERTIFICATE----- +SSA8MyBIYXJ2ZXN0 +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +c3VwZXIgc2VjcmV0 +-----END PRIVATE KEY-----" \ No newline at end of file diff --git a/pkg/auth/testdata/get_cert2 b/pkg/auth/testdata/get_cert2 new file mode 100755 index 000000000..f9334da86 --- /dev/null +++ b/pkg/auth/testdata/get_cert2 @@ -0,0 +1,13 @@ +#!/bin/bash +# Used by pkg/auth/auth_test.go +# echo -n 'I <3 Harvest' | base64 +# SSA8MyBIYXJ2ZXN0 +# echo -n 'super secret' | base64 +# c3VwZXIgc2VjcmV0 +echo "-----BEGIN CERTIFICATE----- +Animal: Owl +SSA8MyBIYXJ2ZXN0 +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +c3VwZXIgc2VjcmV0 +-----END PRIVATE KEY-----" \ No newline at end of file diff --git a/pkg/conf/conf.go b/pkg/conf/conf.go index 54992c7ec..a95f810fb 100644 --- a/pkg/conf/conf.go +++ b/pkg/conf/conf.go @@ -362,6 +362,11 @@ type CredentialsScript struct { Timeout string `yaml:"timeout,omitempty"` } +type CertificateScript struct { + Path string `yaml:"path,omitempty"` + Timeout string `yaml:"timeout,omitempty"` +} + type Poller struct { Addr string `yaml:"addr,omitempty"` APIVersion string `yaml:"api_version,omitempty"` @@ -372,6 +377,7 @@ type Poller struct { Collectors []Collector `yaml:"collectors,omitempty"` CredentialsFile string `yaml:"credentials_file,omitempty"` CredentialsScript CredentialsScript `yaml:"credentials_script,omitempty"` + CertificateScript CertificateScript `yaml:"certificate_script,omitempty"` Datacenter string `yaml:"datacenter,omitempty"` Exporters []string `yaml:"exporters,omitempty"` IsKfs bool `yaml:"is_kfs,omitempty"` @@ -478,6 +484,10 @@ func ZapiPoller(n *node.Node) *Poller { p.CredentialsScript.Schedule = credentialsScriptNode.GetChildContentS("schedule") p.CredentialsScript.Timeout = credentialsScriptNode.GetChildContentS("timeout") } + if certificateScriptNode := n.GetChildS("certificate_script"); certificateScriptNode != nil { + p.CertificateScript.Path = certificateScriptNode.GetChildContentS("path") + p.CertificateScript.Timeout = certificateScriptNode.GetChildContentS("timeout") + } if clientTimeout := n.GetChildContentS("client_timeout"); clientTimeout != "" { p.ClientTimeout = clientTimeout } else { From 7a1d84bec4bd28447c7559634432d97af2c10d3f Mon Sep 17 00:00:00 2001 From: Chris Grindstaff Date: Wed, 26 Jul 2023 14:18:24 -0400 Subject: [PATCH 2/3] feat: Harvest should fetch certificates via a script --- cmd/tools/rest/client.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/tools/rest/client.go b/cmd/tools/rest/client.go index 91b4abbe1..cf69c2d07 100644 --- a/cmd/tools/rest/client.go +++ b/cmd/tools/rest/client.go @@ -81,7 +81,13 @@ func New(poller *conf.Poller, timeout time.Duration, auth *auth.Credentials) (*C if err != nil { return nil, err } - + pollerAuth, err := auth.GetPollerAuth() + if err != nil { + return nil, err + } + if !pollerAuth.IsCert { + client.username = pollerAuth.Username + } transport.DialContext = (&net.Dialer{Timeout: DefaultDialerTimeout}).DialContext httpclient = &http.Client{Transport: transport, Timeout: timeout} client.client = httpclient From 901c3712f975716504daef3402b32faa936bb6f4 Mon Sep 17 00:00:00 2001 From: Chris Grindstaff Date: Fri, 28 Jul 2023 10:18:23 -0400 Subject: [PATCH 3/3] feat: Harvest should fetch certificates via a script --- cmd/collectors/storagegrid/rest/client.go | 29 +++++------ cmd/tools/rest/client.go | 63 +++++++++-------------- cmd/tools/rest/rest.go | 6 ++- pkg/api/ontapi/zapi/client.go | 8 +-- pkg/auth/auth.go | 13 ++--- 5 files changed, 51 insertions(+), 68 deletions(-) diff --git a/cmd/collectors/storagegrid/rest/client.go b/cmd/collectors/storagegrid/rest/client.go index b725ae7b4..00a495ba8 100644 --- a/cmd/collectors/storagegrid/rest/client.go +++ b/cmd/collectors/storagegrid/rest/client.go @@ -26,18 +26,17 @@ const ( ) type Client struct { - client *http.Client - request *http.Request - buffer *bytes.Buffer - Logger *logging.Logger - baseURL string - Cluster Cluster - username string - token string - Timeout time.Duration - logRest bool // used to log Rest request/response - APIPath string - auth *auth.Credentials + client *http.Client + request *http.Request + buffer *bytes.Buffer + Logger *logging.Logger + baseURL string + Cluster Cluster + token string + Timeout time.Duration + logRest bool // used to log Rest request/response + APIPath string + auth *auth.Credentials } type Cluster struct { @@ -338,13 +337,13 @@ func (c *Client) fetchTokenWithAuthRetry() error { if err != nil { return fmt.Errorf("failed to create auth URL err: %w", err) } - password, err := c.auth.Password() + pollerAuth, err := c.auth.GetPollerAuth() if err != nil { return err } authB := authBody{ - Username: c.username, - Password: password, + Username: pollerAuth.Username, + Password: pollerAuth.Password, } postBody, err := json.Marshal(authB) if err != nil { diff --git a/cmd/tools/rest/client.go b/cmd/tools/rest/client.go index cf69c2d07..aa737db0c 100644 --- a/cmd/tools/rest/client.go +++ b/cmd/tools/rest/client.go @@ -31,16 +31,15 @@ const ( ) type Client struct { - client *http.Client - request *http.Request - buffer *bytes.Buffer - Logger *logging.Logger - baseURL string - cluster Cluster - username string - Timeout time.Duration - logRest bool // used to log Rest request/response - auth *auth.Credentials + client *http.Client + request *http.Request + buffer *bytes.Buffer + Logger *logging.Logger + baseURL string + cluster Cluster + Timeout time.Duration + logRest bool // used to log Rest request/response + auth *auth.Credentials } type Cluster struct { @@ -81,13 +80,6 @@ func New(poller *conf.Poller, timeout time.Duration, auth *auth.Credentials) (*C if err != nil { return nil, err } - pollerAuth, err := auth.GetPollerAuth() - if err != nil { - return nil, err - } - if !pollerAuth.IsCert { - client.username = pollerAuth.Username - } transport.DialContext = (&net.Dialer{Timeout: DefaultDialerTimeout}).DialContext httpclient = &http.Client{Transport: transport, Timeout: timeout} client.client = httpclient @@ -135,12 +127,12 @@ func (c *Client) GetRest(request string) ([]byte, error) { return nil, err } c.request.Header.Set("accept", "application/json") - if c.username != "" { - password, err2 := c.auth.Password() - if err2 != nil { - return nil, err2 - } - c.request.SetBasicAuth(c.username, password) + pollerAuth, err := c.auth.GetPollerAuth() + if err != nil { + return nil, err + } + if pollerAuth.Username != "" { + c.request.SetBasicAuth(pollerAuth.Username, pollerAuth.Password) } // ensure that we can change body dynamically c.request.GetBody = func() (io.ReadCloser, error) { @@ -232,11 +224,11 @@ func (c *Client) invokeWithAuthRetry() ([]byte, error) { } if pollerAuth.HasCredentialScript { c.auth.Expire() - password, err2 := c.auth.Password() + pollerAuth2, err2 := c.auth.GetPollerAuth() if err2 != nil { return nil, err2 } - c.request.SetBasicAuth(pollerAuth.Username, password) + c.request.SetBasicAuth(pollerAuth2.Username, pollerAuth2.Password) return doInvoke() } } @@ -246,8 +238,6 @@ func (c *Client) invokeWithAuthRetry() ([]byte, error) { } func downloadSwagger(poller *conf.Poller, path string, url string, verbose bool) (int64, error) { - var restClient *Client - out, err := os.Create(path) if err != nil { return 0, fmt.Errorf("unable to create %s to save swagger.yaml", path) @@ -259,23 +249,18 @@ func downloadSwagger(poller *conf.Poller, path string, url string, verbose bool) } timeout, _ := time.ParseDuration(DefaultTimeout) - if restClient, err = New(poller, timeout, auth.NewCredentials(poller, logging.Get())); err != nil { - return 0, fmt.Errorf("error creating new client %w", err) + credentials := auth.NewCredentials(poller, logging.Get()) + transport, err := credentials.Transport(request) + if err != nil { + return 0, err } + httpclient := &http.Client{Transport: transport, Timeout: timeout} - downClient := &http.Client{Transport: restClient.client.Transport, Timeout: restClient.client.Timeout} - if restClient.username != "" { - password, err2 := restClient.auth.Password() - if err2 != nil { - return 0, err2 - } - request.SetBasicAuth(restClient.username, password) - } if verbose { requestOut, _ := httputil.DumpRequestOut(request, false) - fmt.Printf("REQUEST: %s BY: %s\n%s\n", url, restClient.username, requestOut) + fmt.Printf("REQUEST: %s\n%s\n", url, requestOut) } - response, err := downClient.Do(request) + response, err := httpclient.Do(request) if err != nil { return 0, err } diff --git a/cmd/tools/rest/rest.go b/cmd/tools/rest/rest.go index 416941e52..74e759e1d 100644 --- a/cmd/tools/rest/rest.go +++ b/cmd/tools/rest/rest.go @@ -319,7 +319,11 @@ func FetchForCli(client *Client, href string, records *[]any, downloadAll bool, return fmt.Errorf("error making request %w", err) } - *curls = append(*curls, fmt.Sprintf("curl --user %s --insecure '%s%s'", client.username, client.baseURL, href)) + pollerAuth, err := client.auth.GetPollerAuth() + if err != nil { + return err + } + *curls = append(*curls, fmt.Sprintf("curl --user %s --insecure '%s%s'", pollerAuth.Username, client.baseURL, href)) isNonIterRestCall := false value := gjson.GetBytes(getRest, "records") diff --git a/pkg/api/ontapi/zapi/client.go b/pkg/api/ontapi/zapi/client.go index 574568336..ba201f631 100644 --- a/pkg/api/ontapi/zapi/client.go +++ b/pkg/api/ontapi/zapi/client.go @@ -432,11 +432,11 @@ func (c *Client) invokeWithAuthRetry(withTimers bool) (*node.Node, time.Duration // and try again if errors.Is(he, errs.ErrAuthFailed) && pollerAuth.HasCredentialScript { c.auth.Expire() - password, err := c.auth.Password() - if err != nil { - return nil, 0, 0, err + pollerAuth2, err2 := c.auth.GetPollerAuth() + if err2 != nil { + return nil, 0, 0, err2 } - c.request.SetBasicAuth(pollerAuth.Username, password) + c.request.SetBasicAuth(pollerAuth2.Username, pollerAuth2.Password) c.request.Body = io.NopCloser(&buffer) c.request.ContentLength = int64(buffer.Len()) result2, s1, s2, err3 := c.invoke(withTimers) diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index ae3e8801b..7549ed1e7 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -45,14 +45,6 @@ type Credentials struct { cachedPassword string } -func (c *Credentials) Password() (string, error) { - auth, err := c.GetPollerAuth() - if err != nil { - return "", err - } - return auth.Password, nil -} - // Expire will reset the credential schedule if the receiver has a CredentialsScript // Otherwise it will do nothing. // Resetting the schedule will cause the next call to Password to fetch the credentials @@ -317,10 +309,13 @@ func handCertificateAuth(c *Credentials, poller *conf.Poller, insecureTLS bool) certPath := poller.SslCert keyPath := poller.SslKey - if certPath == "" { + if certPath == "" || keyPath == "" { o := &options.Options{} options.SetPathsAndHostname(o) pathPrefix = path.Join(o.HomePath, "cert/", o.Hostname) + } + + if certPath == "" { certPath = pathPrefix + ".pem" } if keyPath == "" {