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 }