github.com/argoproj/argo-cd/v3@v3.2.1/cmd/argocd/commands/cert.go (about)

     1  package commands
     2  
     3  import (
     4  	"crypto/x509"
     5  	stderrors "errors"
     6  	"fmt"
     7  	"os"
     8  	"sort"
     9  	"strings"
    10  	"text/tabwriter"
    11  
    12  	"github.com/spf13/cobra"
    13  
    14  	"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/headless"
    15  	"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/utils"
    16  	argocdclient "github.com/argoproj/argo-cd/v3/pkg/apiclient"
    17  	certificatepkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/certificate"
    18  	appsv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    19  	certutil "github.com/argoproj/argo-cd/v3/util/cert"
    20  	"github.com/argoproj/argo-cd/v3/util/errors"
    21  	utilio "github.com/argoproj/argo-cd/v3/util/io"
    22  )
    23  
    24  // NewCertCommand returns a new instance of an `argocd repo` command
    25  func NewCertCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
    26  	command := &cobra.Command{
    27  		Use:   "cert",
    28  		Short: "Manage repository certificates and SSH known hosts entries",
    29  		Run: func(c *cobra.Command, args []string) {
    30  			c.HelpFunc()(c, args)
    31  			os.Exit(1)
    32  		},
    33  		Example: `  # Add a TLS certificate for cd.example.com to ArgoCD cert store from a file
    34    argocd cert add-tls --from ~/mycert.pem cd.example.com
    35  
    36    # Add a TLS certificate for cd.example.com to ArgoCD via stdin
    37    cat ~/mycert.pem | argocd cert add-tls cd.example.com
    38  
    39    # Add SSH known host entries for cd.example.com to ArgoCD by scanning host
    40    ssh-keyscan cd.example.com | argocd cert add-ssh --batch
    41  
    42    # List all known TLS certificates
    43    argocd cert list --cert-type https
    44  
    45    # Remove all TLS certificates for cd.example.com
    46    argocd cert rm --cert-type https cd.example.com
    47  
    48    # Remove all certificates and SSH known host entries for cd.example.com
    49    argocd cert rm cd.example.com
    50  `,
    51  	}
    52  
    53  	command.AddCommand(NewCertAddSSHCommand(clientOpts))
    54  	command.AddCommand(NewCertAddTLSCommand(clientOpts))
    55  	command.AddCommand(NewCertListCommand(clientOpts))
    56  	command.AddCommand(NewCertRemoveCommand(clientOpts))
    57  	return command
    58  }
    59  
    60  func NewCertAddTLSCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
    61  	var (
    62  		fromFile string
    63  		upsert   bool
    64  	)
    65  	command := &cobra.Command{
    66  		Use:   "add-tls SERVERNAME",
    67  		Short: "Add TLS certificate data for connecting to repository server SERVERNAME",
    68  		Run: func(c *cobra.Command, args []string) {
    69  			ctx := c.Context()
    70  
    71  			conn, certIf := headless.NewClientOrDie(clientOpts, c).NewCertClientOrDie()
    72  			defer utilio.Close(conn)
    73  
    74  			if len(args) != 1 {
    75  				c.HelpFunc()(c, args)
    76  				os.Exit(1)
    77  			}
    78  
    79  			var certificateArray []string
    80  			var err error
    81  
    82  			if fromFile != "" {
    83  				fmt.Printf("Reading TLS certificate data in PEM format from '%s'\n", fromFile)
    84  				certificateArray, err = certutil.ParseTLSCertificatesFromPath(fromFile)
    85  			} else {
    86  				fmt.Println("Enter TLS certificate data in PEM format. Press CTRL-D when finished.")
    87  				certificateArray, err = certutil.ParseTLSCertificatesFromStream(os.Stdin)
    88  			}
    89  
    90  			errors.CheckError(err)
    91  
    92  			certificateList := make([]appsv1.RepositoryCertificate, 0)
    93  
    94  			subjectMap := make(map[string]*x509.Certificate)
    95  
    96  			for _, entry := range certificateArray {
    97  				// We want to make sure to only send valid certificate data to the
    98  				// server, so we decode the certificate into X509 structure before
    99  				// further processing it.
   100  				x509cert, err := certutil.DecodePEMCertificateToX509(entry)
   101  				errors.CheckError(err)
   102  
   103  				// TODO: We need a better way to detect duplicates sent in the stream,
   104  				// maybe by using fingerprints? For now, no two certs with the same
   105  				// subject may be sent.
   106  				if subjectMap[x509cert.Subject.String()] != nil {
   107  					fmt.Printf("ERROR: Cert with subject '%s' already seen in the input stream.\n", x509cert.Subject.String())
   108  					continue
   109  				}
   110  				subjectMap[x509cert.Subject.String()] = x509cert
   111  			}
   112  
   113  			serverName := args[0]
   114  
   115  			if len(certificateArray) > 0 {
   116  				certificateList = append(certificateList, appsv1.RepositoryCertificate{
   117  					ServerName: serverName,
   118  					CertType:   "https",
   119  					CertData:   []byte(strings.Join(certificateArray, "\n")),
   120  				})
   121  				certificates, err := certIf.CreateCertificate(ctx, &certificatepkg.RepositoryCertificateCreateRequest{
   122  					Certificates: &appsv1.RepositoryCertificateList{
   123  						Items: certificateList,
   124  					},
   125  					Upsert: upsert,
   126  				})
   127  				errors.CheckError(err)
   128  				fmt.Printf("Created entry with %d PEM certificates for repository server %s\n", len(certificates.Items), serverName)
   129  			} else {
   130  				fmt.Printf("No valid certificates have been detected in the stream.\n")
   131  			}
   132  		},
   133  	}
   134  	command.Flags().StringVar(&fromFile, "from", "", "Read TLS certificate data from file (default is to read from stdin)")
   135  	command.Flags().BoolVar(&upsert, "upsert", false, "Replace existing TLS certificate if certificate is different in input")
   136  	return command
   137  }
   138  
   139  // NewCertAddSSHCommand returns a new instance of an `argocd cert add` command
   140  func NewCertAddSSHCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
   141  	var (
   142  		fromFile     string
   143  		batchProcess bool
   144  		upsert       bool
   145  		certificates []appsv1.RepositoryCertificate
   146  	)
   147  
   148  	command := &cobra.Command{
   149  		Use:   "add-ssh --batch",
   150  		Short: "Add SSH known host entries for repository servers",
   151  		Run: func(c *cobra.Command, _ []string) {
   152  			ctx := c.Context()
   153  
   154  			conn, certIf := headless.NewClientOrDie(clientOpts, c).NewCertClientOrDie()
   155  			defer utilio.Close(conn)
   156  
   157  			var sshKnownHostsLists []string
   158  			var err error
   159  
   160  			// --batch is a flag, but it is mandatory for now.
   161  			if batchProcess {
   162  				if fromFile != "" {
   163  					fmt.Printf("Reading SSH known hosts entries from file '%s'\n", fromFile)
   164  					sshKnownHostsLists, err = certutil.ParseSSHKnownHostsFromPath(fromFile)
   165  				} else {
   166  					fmt.Println("Enter SSH known hosts entries, one per line. Press CTRL-D when finished.")
   167  					sshKnownHostsLists, err = certutil.ParseSSHKnownHostsFromStream(os.Stdin)
   168  				}
   169  			} else {
   170  				err = stderrors.New("you need to specify --batch or specify --help for usage instructions")
   171  			}
   172  
   173  			errors.CheckError(err)
   174  
   175  			if len(sshKnownHostsLists) == 0 {
   176  				errors.Fatal(errors.ErrorGeneric, "No valid SSH known hosts data found.")
   177  			}
   178  
   179  			for _, knownHostsEntry := range sshKnownHostsLists {
   180  				_, certSubType, certData, err := certutil.TokenizeSSHKnownHostsEntry(knownHostsEntry)
   181  				errors.CheckError(err)
   182  				hostnameList, _, err := certutil.KnownHostsLineToPublicKey(knownHostsEntry)
   183  				errors.CheckError(err)
   184  				// Each key could be valid for multiple hostnames
   185  				for _, hostname := range hostnameList {
   186  					certificate := appsv1.RepositoryCertificate{
   187  						ServerName:  hostname,
   188  						CertType:    "ssh",
   189  						CertSubType: certSubType,
   190  						CertData:    certData,
   191  					}
   192  					certificates = append(certificates, certificate)
   193  				}
   194  			}
   195  
   196  			certList := &appsv1.RepositoryCertificateList{Items: certificates}
   197  			response, err := certIf.CreateCertificate(ctx, &certificatepkg.RepositoryCertificateCreateRequest{
   198  				Certificates: certList,
   199  				Upsert:       upsert,
   200  			})
   201  			errors.CheckError(err)
   202  			fmt.Printf("Successfully created %d SSH known host entries\n", len(response.Items))
   203  		},
   204  	}
   205  	command.Flags().StringVar(&fromFile, "from", "", "Read SSH known hosts data from file (default is to read from stdin)")
   206  	command.Flags().BoolVar(&batchProcess, "batch", false, "Perform batch processing by reading in SSH known hosts data (mandatory flag)")
   207  	command.Flags().BoolVar(&upsert, "upsert", false, "Replace existing SSH server public host keys if key is different in input")
   208  	return command
   209  }
   210  
   211  // NewCertRemoveCommand returns a new instance of an `argocd cert rm` command
   212  func NewCertRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
   213  	var (
   214  		certType    string
   215  		certSubType string
   216  		certQuery   certificatepkg.RepositoryCertificateQuery
   217  	)
   218  	command := &cobra.Command{
   219  		Use:   "rm REPOSERVER",
   220  		Short: "Remove certificate of TYPE for REPOSERVER",
   221  		Run: func(c *cobra.Command, args []string) {
   222  			ctx := c.Context()
   223  
   224  			if len(args) < 1 {
   225  				c.HelpFunc()(c, args)
   226  				os.Exit(1)
   227  			}
   228  			conn, certIf := headless.NewClientOrDie(clientOpts, c).NewCertClientOrDie()
   229  			defer utilio.Close(conn)
   230  			hostNamePattern := args[0]
   231  
   232  			// Prevent the user from specifying a wildcard as hostname as precaution
   233  			// measure -- the user could still use "?*" or any other pattern to
   234  			// remove all certificates, but it's less likely that it happens by
   235  			// accident.
   236  			if hostNamePattern == "*" {
   237  				errors.Fatal(errors.ErrorGeneric, "A single wildcard is not allowed as REPOSERVER name.")
   238  			}
   239  
   240  			promptUtil := utils.NewPrompt(clientOpts.PromptsEnabled)
   241  			canDelete := promptUtil.Confirm(fmt.Sprintf("Are you sure you want to remove all certificates for '%s'? [y/n]", hostNamePattern))
   242  			if canDelete {
   243  				certQuery = certificatepkg.RepositoryCertificateQuery{
   244  					HostNamePattern: hostNamePattern,
   245  					CertType:        certType,
   246  					CertSubType:     certSubType,
   247  				}
   248  				removed, err := certIf.DeleteCertificate(ctx, &certQuery)
   249  				errors.CheckError(err)
   250  				if len(removed.Items) > 0 {
   251  					for _, cert := range removed.Items {
   252  						fmt.Printf("Removed cert for '%s' of type '%s' (subtype '%s')\n", cert.ServerName, cert.CertType, cert.CertSubType)
   253  					}
   254  				} else {
   255  					fmt.Println("No certificates were removed (none matched the given pattern)")
   256  				}
   257  			} else {
   258  				fmt.Printf("The command to remove all certificates for '%s' was cancelled.\n", hostNamePattern)
   259  			}
   260  		},
   261  	}
   262  	command.Flags().StringVar(&certType, "cert-type", "", "Only remove certs of given type (ssh, https)")
   263  	command.Flags().StringVar(&certSubType, "cert-sub-type", "", "Only remove certs of given sub-type (only for ssh)")
   264  	return command
   265  }
   266  
   267  // NewCertListCommand returns a new instance of an `argocd cert rm` command
   268  func NewCertListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
   269  	var (
   270  		certType        string
   271  		hostNamePattern string
   272  		sortOrder       string
   273  		output          string
   274  	)
   275  	command := &cobra.Command{
   276  		Use:   "list",
   277  		Short: "List configured certificates",
   278  		Run: func(c *cobra.Command, _ []string) {
   279  			ctx := c.Context()
   280  
   281  			if certType != "" {
   282  				switch certType {
   283  				case "ssh":
   284  				case "https":
   285  				default:
   286  					fmt.Println("cert-type must be either ssh or https")
   287  					os.Exit(1)
   288  				}
   289  			}
   290  
   291  			conn, certIf := headless.NewClientOrDie(clientOpts, c).NewCertClientOrDie()
   292  			defer utilio.Close(conn)
   293  			certificates, err := certIf.ListCertificates(ctx, &certificatepkg.RepositoryCertificateQuery{HostNamePattern: hostNamePattern, CertType: certType})
   294  			errors.CheckError(err)
   295  
   296  			switch output {
   297  			case "yaml", "json":
   298  				err := PrintResourceList(certificates.Items, output, false)
   299  				errors.CheckError(err)
   300  			case "wide", "":
   301  				printCertTable(certificates.Items, sortOrder)
   302  			default:
   303  				errors.CheckError(fmt.Errorf("unknown output format: %s", output))
   304  			}
   305  		},
   306  	}
   307  
   308  	command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
   309  	command.Flags().StringVar(&sortOrder, "sort", "", "Set display sort order for output format wide. One of: hostname|type")
   310  	command.Flags().StringVar(&certType, "cert-type", "", "Only list certificates of given type, valid: 'ssh','https'")
   311  	command.Flags().StringVar(&hostNamePattern, "hostname-pattern", "", "Only list certificates for hosts matching given glob-pattern")
   312  	return command
   313  }
   314  
   315  // Print table of certificate info
   316  func printCertTable(certs []appsv1.RepositoryCertificate, sortOrder string) {
   317  	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   318  	fmt.Fprintf(w, "HOSTNAME\tTYPE\tSUBTYPE\tINFO\n")
   319  
   320  	switch sortOrder {
   321  	case "hostname", "":
   322  		sort.Slice(certs, func(i, j int) bool {
   323  			return certs[i].ServerName < certs[j].ServerName
   324  		})
   325  	case "type":
   326  		sort.Slice(certs, func(i, j int) bool {
   327  			return certs[i].CertType < certs[j].CertType
   328  		})
   329  	}
   330  
   331  	for _, c := range certs {
   332  		fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", c.ServerName, c.CertType, c.CertSubType, c.CertInfo)
   333  	}
   334  	_ = w.Flush()
   335  }