diff --git a/changelogs/unreleased/2364-mansam b/changelogs/unreleased/2364-mansam new file mode 100644 index 000000000..28c37c5b9 --- /dev/null +++ b/changelogs/unreleased/2364-mansam @@ -0,0 +1 @@ +Added a `--cacert` flag to the velero client describe, download, and logs commands to allow passing a path to a certificate to use when verifying TLS connections to object storage. Also added a corresponding client config option called `cacert` which takes a path to a certificate bundle to use as a default when `--cacert` is not specified. diff --git a/pkg/client/config.go b/pkg/client/config.go index 9a943311d..2df34c619 100644 --- a/pkg/client/config.go +++ b/pkg/client/config.go @@ -28,6 +28,7 @@ import ( const ( ConfigKeyNamespace = "namespace" ConfigKeyFeatures = "features" + ConfigKeyCACert = "cacert" ) // VeleroConfig is a map of strings to interface{} for deserializing Velero client config options. @@ -110,6 +111,19 @@ func (c VeleroConfig) Features() []string { return strings.Split(features, ",") } +func (c VeleroConfig) CACertFile() string { + val, ok := c[ConfigKeyCACert] + if !ok { + return "" + } + caCertFile, ok := val.(string) + if !ok { + return "" + } + + return caCertFile +} + func configFileName() string { return filepath.Join(os.Getenv("HOME"), ".config", "velero", "config.json") } diff --git a/pkg/cmd/cli/backup/describe.go b/pkg/cmd/cli/backup/describe.go index 45b61e2cf..536776ba5 100644 --- a/pkg/cmd/cli/backup/describe.go +++ b/pkg/cmd/cli/backup/describe.go @@ -38,6 +38,12 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { insecureSkipTLSVerify bool ) + config, err := client.LoadConfig() + if err != nil { + fmt.Fprintf(os.Stderr, "WARNING: Error reading config file: %v\n", err) + } + caCertFile := config.CACertFile() + c := &cobra.Command{ Use: use + " [NAME1] [NAME2] [NAME...]", Short: "Describe backups", @@ -72,7 +78,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { fmt.Fprintf(os.Stderr, "error getting PodVolumeBackups for backup %s: %v\n", backup.Name, err) } - s := output.DescribeBackup(&backup, deleteRequestList.Items, podVolumeBackupList.Items, details, veleroClient, insecureSkipTLSVerify) + s := output.DescribeBackup(&backup, deleteRequestList.Items, podVolumeBackupList.Items, details, veleroClient, insecureSkipTLSVerify, caCertFile) if first { first = false fmt.Print(s) @@ -87,6 +93,6 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "only show items matching this label selector") c.Flags().BoolVar(&details, "details", details, "display additional detail in the command output") c.Flags().BoolVar(&insecureSkipTLSVerify, "insecure-skip-tls-verify", insecureSkipTLSVerify, "If true, the object store's TLS certificate will not be checked for validity. This is insecure and susceptible to man-in-the-middle attacks. Not recommended for production.") - + c.Flags().StringVar(&caCertFile, "cacert", caCertFile, "path to a certificate bundle to use when verifying TLS connections") return c } diff --git a/pkg/cmd/cli/backup/download.go b/pkg/cmd/cli/backup/download.go index 91b0ec567..99f4f089e 100644 --- a/pkg/cmd/cli/backup/download.go +++ b/pkg/cmd/cli/backup/download.go @@ -34,7 +34,13 @@ import ( ) func NewDownloadCommand(f client.Factory) *cobra.Command { + config, err := client.LoadConfig() + if err != nil { + fmt.Fprintf(os.Stderr, "WARNING: Error reading config file: %v\n", err) + } o := NewDownloadOptions() + o.caCertFile = config.CACertFile() + c := &cobra.Command{ Use: "download NAME", Short: "Download a backup", @@ -58,6 +64,7 @@ type DownloadOptions struct { Timeout time.Duration InsecureSkipTLSVerify bool writeOptions int + caCertFile string } func NewDownloadOptions() *DownloadOptions { @@ -71,6 +78,8 @@ func (o *DownloadOptions) BindFlags(flags *pflag.FlagSet) { flags.BoolVar(&o.Force, "force", o.Force, "forces the download and will overwrite file if it exists already") flags.DurationVar(&o.Timeout, "timeout", o.Timeout, "maximum time to wait to process download request") flags.BoolVar(&o.InsecureSkipTLSVerify, "insecure-skip-tls-verify", o.InsecureSkipTLSVerify, "If true, the object store's TLS certificate will not be checked for validity. This is insecure and susceptible to man-in-the-middle attacks. Not recommended for production.") + flags.StringVar(&o.caCertFile, "cacert", o.caCertFile, "path to a certificate bundle to use when verifying TLS connections") + } func (o *DownloadOptions) Validate(c *cobra.Command, args []string, f client.Factory) error { @@ -113,7 +122,7 @@ func (o *DownloadOptions) Run(c *cobra.Command, f client.Factory) error { } defer backupDest.Close() - err = downloadrequest.Stream(veleroClient.VeleroV1(), f.Namespace(), o.Name, v1.DownloadTargetKindBackupContents, backupDest, o.Timeout, o.InsecureSkipTLSVerify) + err = downloadrequest.Stream(veleroClient.VeleroV1(), f.Namespace(), o.Name, v1.DownloadTargetKindBackupContents, backupDest, o.Timeout, o.InsecureSkipTLSVerify, o.caCertFile) if err != nil { os.Remove(o.Output) cmd.CheckError(err) diff --git a/pkg/cmd/cli/backup/logs.go b/pkg/cmd/cli/backup/logs.go index 16882b087..3e64b68ab 100644 --- a/pkg/cmd/cli/backup/logs.go +++ b/pkg/cmd/cli/backup/logs.go @@ -17,6 +17,7 @@ limitations under the License. package backup import ( + "fmt" "os" "time" @@ -31,8 +32,14 @@ import ( ) func NewLogsCommand(f client.Factory) *cobra.Command { + config, err := client.LoadConfig() + if err != nil { + fmt.Fprintf(os.Stderr, "WARNING: Error reading config file: %v\n", err) + } + timeout := time.Minute insecureSkipTLSVerify := false + caCertFile := config.CACertFile() c := &cobra.Command{ Use: "logs BACKUP", @@ -59,13 +66,13 @@ func NewLogsCommand(f client.Factory) *cobra.Command { "until the backup has a phase of Completed or Failed and try again.", backupName) } - err = downloadrequest.Stream(veleroClient.VeleroV1(), f.Namespace(), backupName, v1.DownloadTargetKindBackupLog, os.Stdout, timeout, insecureSkipTLSVerify) + err = downloadrequest.Stream(veleroClient.VeleroV1(), f.Namespace(), backupName, v1.DownloadTargetKindBackupLog, os.Stdout, timeout, insecureSkipTLSVerify, caCertFile) cmd.CheckError(err) }, } c.Flags().DurationVar(&timeout, "timeout", timeout, "how long to wait to receive logs") c.Flags().BoolVar(&insecureSkipTLSVerify, "insecure-skip-tls-verify", insecureSkipTLSVerify, "If true, the object store's TLS certificate will not be checked for validity. This is insecure and susceptible to man-in-the-middle attacks. Not recommended for production.") - + c.Flags().StringVar(&caCertFile, "cacert", caCertFile, "path to a certificate bundle to use when verifying TLS connections") return c } diff --git a/pkg/cmd/cli/restore/describe.go b/pkg/cmd/cli/restore/describe.go index f90fa6965..e6028bcab 100644 --- a/pkg/cmd/cli/restore/describe.go +++ b/pkg/cmd/cli/restore/describe.go @@ -37,6 +37,12 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { insecureSkipTLSVerify bool ) + config, err := client.LoadConfig() + if err != nil { + fmt.Fprintf(os.Stderr, "WARNING: Error reading config file: %v\n", err) + } + caCertFile := config.CACertFile() + c := &cobra.Command{ Use: use + " [NAME1] [NAME2] [NAME...]", Short: "Describe restores", @@ -65,7 +71,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { fmt.Fprintf(os.Stderr, "error getting PodVolumeRestores for restore %s: %v\n", restore.Name, err) } - s := output.DescribeRestore(&restore, podvolumeRestoreList.Items, details, veleroClient, insecureSkipTLSVerify) + s := output.DescribeRestore(&restore, podvolumeRestoreList.Items, details, veleroClient, insecureSkipTLSVerify, caCertFile) if first { first = false fmt.Print(s) @@ -80,6 +86,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "only show items matching this label selector") c.Flags().BoolVar(&details, "details", details, "display additional detail in the command output") c.Flags().BoolVar(&insecureSkipTLSVerify, "insecure-skip-tls-verify", insecureSkipTLSVerify, "If true, the object store's TLS certificate will not be checked for validity. This is insecure and susceptible to man-in-the-middle attacks. Not recommended for production.") + c.Flags().StringVar(&caCertFile, "cacert", caCertFile, "path to a certificate bundle to use when verifying TLS connections") return c } diff --git a/pkg/cmd/cli/restore/logs.go b/pkg/cmd/cli/restore/logs.go index 7baf8e599..592884c5f 100644 --- a/pkg/cmd/cli/restore/logs.go +++ b/pkg/cmd/cli/restore/logs.go @@ -17,6 +17,7 @@ limitations under the License. package restore import ( + "fmt" "os" "time" @@ -31,8 +32,14 @@ import ( ) func NewLogsCommand(f client.Factory) *cobra.Command { + config, err := client.LoadConfig() + if err != nil { + fmt.Fprintf(os.Stderr, "WARNING: Error reading config file: %v\n", err) + } + timeout := time.Minute insecureSkipTLSVerify := false + caCertFile := config.CACertFile() c := &cobra.Command{ Use: "logs RESTORE", @@ -59,13 +66,14 @@ func NewLogsCommand(f client.Factory) *cobra.Command { "until the restore has a phase of Completed or Failed and try again.", restoreName) } - err = downloadrequest.Stream(veleroClient.VeleroV1(), f.Namespace(), restoreName, v1.DownloadTargetKindRestoreLog, os.Stdout, timeout, insecureSkipTLSVerify) + err = downloadrequest.Stream(veleroClient.VeleroV1(), f.Namespace(), restoreName, v1.DownloadTargetKindRestoreLog, os.Stdout, timeout, insecureSkipTLSVerify, caCertFile) cmd.CheckError(err) }, } c.Flags().DurationVar(&timeout, "timeout", timeout, "how long to wait to receive logs") c.Flags().BoolVar(&insecureSkipTLSVerify, "insecure-skip-tls-verify", insecureSkipTLSVerify, "If true, the object store's TLS certificate will not be checked for validity. This is insecure and susceptible to man-in-the-middle attacks. Not recommended for production.") + c.Flags().StringVar(&caCertFile, "cacert", caCertFile, "path to a certificate bundle to use when verifying TLS connections") return c } diff --git a/pkg/cmd/util/downloadrequest/downloadrequest.go b/pkg/cmd/util/downloadrequest/downloadrequest.go index 1ff3bbbbc..607a75cd2 100644 --- a/pkg/cmd/util/downloadrequest/downloadrequest.go +++ b/pkg/cmd/util/downloadrequest/downloadrequest.go @@ -39,7 +39,7 @@ import ( // not found var ErrNotFound = errors.New("file not found") -func Stream(client velerov1client.DownloadRequestsGetter, namespace, name string, kind v1.DownloadTargetKind, w io.Writer, timeout time.Duration, insecureSkipTLSVerify bool) error { +func Stream(client velerov1client.DownloadRequestsGetter, namespace, name string, kind v1.DownloadTargetKind, w io.Writer, timeout time.Duration, insecureSkipTLSVerify bool, caCertFile string) error { req := &v1.DownloadRequest{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, @@ -99,11 +99,38 @@ Loop: return ErrNotFound } - httpClient := new(http.Client) - if insecureSkipTLSVerify { - httpClient.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + var caPool *x509.CertPool + if len(caCertFile) > 0 { + caCert, err := ioutil.ReadFile(caCertFile) + if err != nil { + return errors.Wrapf(err, "couldn't open cacert") } + // bundle the passed in cert with the system cert pool + // if it's available, otherwise create a new pool just + // for this. + caPool, err = x509.SystemCertPool() + if err != nil { + caPool = x509.NewCertPool() + } + caPool.AppendCertsFromPEM(caCert) + } + + defaultTransport := http.DefaultTransport.(*http.Transport) + // same settings as the default transport + // aside from timeout and TLSClientConfig + httpClient := new(http.Client) + httpClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: insecureSkipTLSVerify, + RootCAs: caPool, + }, + IdleConnTimeout: timeout, + DialContext: defaultTransport.DialContext, + ForceAttemptHTTP2: defaultTransport.ForceAttemptHTTP2, + MaxIdleConns: defaultTransport.MaxIdleConns, + Proxy: defaultTransport.Proxy, + TLSHandshakeTimeout: defaultTransport.TLSHandshakeTimeout, + ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout, } httpReq, err := http.NewRequest("GET", req.Status.DownloadURL, nil) diff --git a/pkg/cmd/util/downloadrequest/downloadrequest_test.go b/pkg/cmd/util/downloadrequest/downloadrequest_test.go index 281809c5a..d6940fadc 100644 --- a/pkg/cmd/util/downloadrequest/downloadrequest_test.go +++ b/pkg/cmd/util/downloadrequest/downloadrequest_test.go @@ -151,7 +151,7 @@ func TestStream(t *testing.T) { output := new(bytes.Buffer) errCh := make(chan error) go func() { - err := Stream(client.VeleroV1(), "namespace", "name", test.kind, output, timeout, false) + err := Stream(client.VeleroV1(), "namespace", "name", test.kind, output, timeout, false, "") errCh <- err }() diff --git a/pkg/cmd/util/output/backup_describer.go b/pkg/cmd/util/output/backup_describer.go index e788f89ca..137fd3819 100644 --- a/pkg/cmd/util/output/backup_describer.go +++ b/pkg/cmd/util/output/backup_describer.go @@ -39,6 +39,7 @@ func DescribeBackup( details bool, veleroClient clientset.Interface, insecureSkipTLSVerify bool, + caCertFile string, ) string { return Describe(func(d *Describer) { d.DescribeMetadata(backup.ObjectMeta) @@ -75,7 +76,7 @@ func DescribeBackup( DescribeBackupSpec(d, backup.Spec) d.Println() - DescribeBackupStatus(d, backup, details, veleroClient, insecureSkipTLSVerify) + DescribeBackupStatus(d, backup, details, veleroClient, insecureSkipTLSVerify, caCertFile) if len(deleteRequests) > 0 { d.Println() @@ -212,7 +213,7 @@ func DescribeBackupSpec(d *Describer, spec velerov1api.BackupSpec) { } // DescribeBackupStatus describes a backup status in human-readable format. -func DescribeBackupStatus(d *Describer, backup *velerov1api.Backup, details bool, veleroClient clientset.Interface, insecureSkipTLSVerify bool) { +func DescribeBackupStatus(d *Describer, backup *velerov1api.Backup, details bool, veleroClient clientset.Interface, insecureSkipTLSVerify bool, caCertPath string) { status := backup.Status d.Printf("Backup Format Version:\t%d\n", status.Version) @@ -238,7 +239,7 @@ func DescribeBackupStatus(d *Describer, backup *velerov1api.Backup, details bool d.Println() if details { - describeBackupResourceList(d, backup, veleroClient, insecureSkipTLSVerify) + describeBackupResourceList(d, backup, veleroClient, insecureSkipTLSVerify, caCertPath) d.Println() } @@ -249,7 +250,7 @@ func DescribeBackupStatus(d *Describer, backup *velerov1api.Backup, details bool } buf := new(bytes.Buffer) - if err := downloadrequest.Stream(veleroClient.VeleroV1(), backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupVolumeSnapshots, buf, downloadRequestTimeout, insecureSkipTLSVerify); err != nil { + if err := downloadrequest.Stream(veleroClient.VeleroV1(), backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupVolumeSnapshots, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil { d.Printf("Persistent Volumes:\t\n", err) return } @@ -270,9 +271,9 @@ func DescribeBackupStatus(d *Describer, backup *velerov1api.Backup, details bool d.Printf("Persistent Volumes: \n") } -func describeBackupResourceList(d *Describer, backup *velerov1api.Backup, veleroClient clientset.Interface, insecureSkipTLSVerify bool) { +func describeBackupResourceList(d *Describer, backup *velerov1api.Backup, veleroClient clientset.Interface, insecureSkipTLSVerify bool, caCertPath string) { buf := new(bytes.Buffer) - if err := downloadrequest.Stream(veleroClient.VeleroV1(), backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupResourceList, buf, downloadRequestTimeout, insecureSkipTLSVerify); err != nil { + if err := downloadrequest.Stream(veleroClient.VeleroV1(), backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupResourceList, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil { if err == downloadrequest.ErrNotFound { // the backup resource list could be missing if (other reasons may exist as well): // - the backup was taken prior to v1.1; or diff --git a/pkg/cmd/util/output/restore_describer.go b/pkg/cmd/util/output/restore_describer.go index 459cda57f..a4e9119fb 100644 --- a/pkg/cmd/util/output/restore_describer.go +++ b/pkg/cmd/util/output/restore_describer.go @@ -31,7 +31,7 @@ import ( pkgrestore "github.com/vmware-tanzu/velero/pkg/restore" ) -func DescribeRestore(restore *v1.Restore, podVolumeRestores []v1.PodVolumeRestore, details bool, veleroClient clientset.Interface, insecureSkipTLSVerify bool) string { +func DescribeRestore(restore *v1.Restore, podVolumeRestores []v1.PodVolumeRestore, details bool, veleroClient clientset.Interface, insecureSkipTLSVerify bool, caCertFile string) string { return Describe(func(d *Describer) { d.DescribeMetadata(restore.ObjectMeta) @@ -56,7 +56,7 @@ func DescribeRestore(restore *v1.Restore, podVolumeRestores []v1.PodVolumeRestor } } - describeRestoreResults(d, restore, veleroClient, insecureSkipTLSVerify) + describeRestoreResults(d, restore, veleroClient, insecureSkipTLSVerify, caCertFile) d.Println() d.Printf("Backup:\t%s\n", restore.Spec.BackupName) @@ -114,7 +114,7 @@ func DescribeRestore(restore *v1.Restore, podVolumeRestores []v1.PodVolumeRestor }) } -func describeRestoreResults(d *Describer, restore *v1.Restore, veleroClient clientset.Interface, insecureSkipTLSVerify bool) { +func describeRestoreResults(d *Describer, restore *v1.Restore, veleroClient clientset.Interface, insecureSkipTLSVerify bool, caCertPath string) { if restore.Status.Warnings == 0 && restore.Status.Errors == 0 { return } @@ -122,7 +122,7 @@ func describeRestoreResults(d *Describer, restore *v1.Restore, veleroClient clie var buf bytes.Buffer var resultMap map[string]pkgrestore.Result - if err := downloadrequest.Stream(veleroClient.VeleroV1(), restore.Namespace, restore.Name, v1.DownloadTargetKindRestoreResults, &buf, downloadRequestTimeout, insecureSkipTLSVerify); err != nil { + if err := downloadrequest.Stream(veleroClient.VeleroV1(), restore.Namespace, restore.Name, v1.DownloadTargetKindRestoreResults, &buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil { d.Printf("Warnings:\t\n\nErrors:\t\n", err, err) return }