github.com/argoproj/argo-cd/v3@v3.2.1/util/db/certificate.go (about) 1 package db 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "regexp" 8 "strings" 9 10 "golang.org/x/crypto/ssh" 11 12 log "github.com/sirupsen/logrus" 13 14 "github.com/argoproj/argo-cd/v3/common" 15 appsv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 16 certutil "github.com/argoproj/argo-cd/v3/util/cert" 17 ) 18 19 // A struct representing an entry in the list of SSH known hosts. 20 type SSHKnownHostsEntry struct { 21 // Hostname the key is for 22 Host string 23 // The type of the key 24 SubType string 25 // The data of the key, including the type 26 Data string 27 // The SHA256 fingerprint of the key 28 Fingerprint string 29 } 30 31 // A representation of a TLS certificate 32 type TLSCertificate struct { 33 // Subject of the certificate 34 Subject string 35 // Issuer of the certificate 36 Issuer string 37 // Certificate data 38 Data string 39 } 40 41 // Helper struct for certificate selection 42 type CertificateListSelector struct { 43 // Pattern to match the hostname with 44 HostNamePattern string 45 // Type of certificate to match 46 CertType string 47 // Subtype of certificate to match 48 CertSubType string 49 } 50 51 // Get a list of all configured repository certificates matching the given 52 // selector. The list of certificates explicitly excludes the CertData of 53 // the certificates, and only returns the metadata including CertInfo field. 54 // 55 // The CertInfo field in the returned entries will contain the following data: 56 // - For SSH keys, the SHA256 fingerprint of the key as string, prepended by 57 // the string "SHA256:" 58 // - For TLS certs, the Subject of the X509 cert as a string in DN notation 59 func (db *db) ListRepoCertificates(_ context.Context, selector *CertificateListSelector) (*appsv1.RepositoryCertificateList, error) { 60 // selector may be given as nil, but we need at least an empty data structure 61 // so we create it if necessary. 62 if selector == nil { 63 selector = &CertificateListSelector{} 64 } 65 66 certificates := make([]appsv1.RepositoryCertificate, 0) 67 68 // Get all SSH known host entries 69 if selector.CertType == "" || selector.CertType == "*" || selector.CertType == "ssh" { 70 sshKnownHosts, err := db.getSSHKnownHostsData() 71 if err != nil { 72 return nil, err 73 } 74 75 for _, entry := range sshKnownHosts { 76 if certutil.MatchHostName(entry.Host, selector.HostNamePattern) && (selector.CertSubType == "" || selector.CertSubType == "*" || selector.CertSubType == entry.SubType) { 77 certificates = append(certificates, appsv1.RepositoryCertificate{ 78 ServerName: entry.Host, 79 CertType: "ssh", 80 CertSubType: entry.SubType, 81 CertInfo: "SHA256:" + certutil.SSHFingerprintSHA256FromString(fmt.Sprintf("%s %s", entry.Host, entry.Data)), 82 }) 83 } 84 } 85 } 86 87 // Get all TLS certificates 88 if selector.CertType == "" || selector.CertType == "*" || selector.CertType == "https" || selector.CertType == "tls" { 89 tlsCertificates, err := db.getTLSCertificateData() 90 if err != nil { 91 return nil, err 92 } 93 for _, entry := range tlsCertificates { 94 if certutil.MatchHostName(entry.Subject, selector.HostNamePattern) { 95 pemEntries, err := certutil.ParseTLSCertificatesFromData(entry.Data) 96 if err != nil { 97 continue 98 } 99 for _, pemEntry := range pemEntries { 100 var certInfo, certSubType string 101 x509Data, err := certutil.DecodePEMCertificateToX509(pemEntry) 102 if err != nil { 103 certInfo = err.Error() 104 certSubType = "invalid" 105 } else { 106 certInfo = x509Data.Subject.String() 107 certSubType = x509Data.PublicKeyAlgorithm.String() 108 } 109 certificates = append(certificates, appsv1.RepositoryCertificate{ 110 ServerName: entry.Subject, 111 CertType: "https", 112 CertSubType: strings.ToLower(certSubType), 113 CertInfo: certInfo, 114 }) 115 } 116 } 117 } 118 } 119 120 return &appsv1.RepositoryCertificateList{ 121 Items: certificates, 122 }, nil 123 } 124 125 // Get a single certificate from the datastore 126 func (db *db) GetRepoCertificate(_ context.Context, serverType string, serverName string) (*appsv1.RepositoryCertificate, error) { 127 if serverType == "ssh" { 128 sshKnownHostsList, err := db.getSSHKnownHostsData() 129 if err != nil { 130 return nil, err 131 } 132 for _, entry := range sshKnownHostsList { 133 if entry.Host == serverName { 134 repo := &appsv1.RepositoryCertificate{ 135 ServerName: entry.Host, 136 CertType: "ssh", 137 CertSubType: entry.SubType, 138 CertData: []byte(entry.Data), 139 CertInfo: entry.Fingerprint, 140 } 141 return repo, nil 142 } 143 } 144 } 145 146 // Fail 147 return nil, nil 148 } 149 150 // Create one or more repository certificates and returns a list of certificates 151 // actually created. 152 func (db *db) CreateRepoCertificate(ctx context.Context, certificates *appsv1.RepositoryCertificateList, upsert bool) (*appsv1.RepositoryCertificateList, error) { 153 var ( 154 saveSSHData = false 155 saveTLSData = false 156 ) 157 158 sshKnownHostsList, err := db.getSSHKnownHostsData() 159 if err != nil { 160 return nil, err 161 } 162 163 tlsCertificates, err := db.getTLSCertificateData() 164 if err != nil { 165 return nil, err 166 } 167 168 // This will hold the final list of certificates that have been created 169 created := make([]appsv1.RepositoryCertificate, 0) 170 171 // Each request can contain multiple certificates of different types, so we 172 // make sure to handle each request accordingly. 173 for _, certificate := range certificates.Items { 174 // Ensure valid repo server name was given only for https certificates. 175 // For SSH known host entries, we let Go's ssh library do the validation 176 // later on. 177 if certificate.CertType == "https" && !certutil.IsValidHostname(certificate.ServerName, false) { 178 return nil, fmt.Errorf("invalid hostname in request: %s", certificate.ServerName) 179 } else if certificate.CertType == "ssh" { 180 // Matches "[hostname]:port" format 181 reExtract := regexp.MustCompile(`^\[(.*)\]:\d+$`) 182 matches := reExtract.FindStringSubmatch(certificate.ServerName) 183 var hostnameToCheck string 184 if len(matches) == 0 { 185 hostnameToCheck = certificate.ServerName 186 } else { 187 hostnameToCheck = matches[1] 188 } 189 if !certutil.IsValidHostname(hostnameToCheck, false) { 190 return nil, fmt.Errorf("invalid hostname in request: %s", hostnameToCheck) 191 } 192 } 193 194 switch certificate.CertType { 195 case "ssh": 196 // Whether we have a new certificate entry 197 newEntry := true 198 // Whether we have upserted an existing certificate entry 199 upserted := false 200 201 // Check whether known hosts entry already exists. Must match hostname 202 // and the key sub type (e.g. ssh-rsa). It is considered an error if we 203 // already have a corresponding key and upsert was not specified. 204 for _, entry := range sshKnownHostsList { 205 if entry.Host == certificate.ServerName && entry.SubType == certificate.CertSubType { 206 if !upsert && entry.Data != string(certificate.CertData) { 207 return nil, fmt.Errorf("key for '%s' (subtype: '%s') already exists, and upsert was not specified", entry.Host, entry.SubType) 208 } 209 // Do not add an entry on upsert, but remember if we actual did an 210 // upsert. 211 newEntry = false 212 if entry.Data != string(certificate.CertData) { 213 entry.Data = string(certificate.CertData) 214 upserted = true 215 } 216 break 217 } 218 } 219 220 // Make sure that we received a valid public host key by parsing it 221 _, hostnames, rawKeyData, _, _, err := ssh.ParseKnownHosts([]byte(fmt.Sprintf("%s %s %s", certificate.ServerName, certificate.CertSubType, certificate.CertData))) 222 if err != nil { 223 return nil, err 224 } 225 226 if len(hostnames) == 0 { 227 log.Errorf("Could not parse hostname for key from token %s", certificate.ServerName) 228 } 229 230 if newEntry { 231 sshKnownHostsList = append(sshKnownHostsList, &SSHKnownHostsEntry{ 232 Host: hostnames[0], 233 Data: string(certificate.CertData), 234 SubType: certificate.CertSubType, 235 }) 236 } 237 238 // If we created a new entry, or if we upserted an existing one, we need 239 // to save the data and notify the consumer about the operation. 240 if newEntry || upserted { 241 certificate.CertInfo = certutil.SSHFingerprintSHA256(rawKeyData) 242 created = append(created, certificate) 243 saveSSHData = true 244 } 245 case "https": 246 var tlsCertificate *TLSCertificate 247 newEntry := true 248 upserted := false 249 pemCreated := make([]string, 0) 250 251 for _, entry := range tlsCertificates { 252 // We have an entry for this server already. Check for upsert. 253 if entry.Subject == certificate.ServerName { 254 newEntry = false 255 if entry.Data != string(certificate.CertData) { 256 if !upsert { 257 return nil, fmt.Errorf("TLS certificate for server '%s' already exists, and upsert was not specified", entry.Subject) 258 } 259 } 260 // Store pointer to this entry for later use. 261 tlsCertificate = entry 262 break 263 } 264 } 265 266 // Check for validity of data received 267 pemData, err := certutil.ParseTLSCertificatesFromData(string(certificate.CertData)) 268 if err != nil { 269 return nil, err 270 } 271 272 // We should have at least one valid PEM entry 273 if len(pemData) == 0 { 274 return nil, errors.New("no valid PEM data received") 275 } 276 277 // Make sure we have valid X509 certificates in the data 278 for _, entry := range pemData { 279 _, err := certutil.DecodePEMCertificateToX509(entry) 280 if err != nil { 281 return nil, err 282 } 283 pemCreated = append(pemCreated, entry) 284 } 285 286 // New certificate if pointer to existing cert is nil 287 if tlsCertificate == nil { 288 tlsCertificate = &TLSCertificate{ 289 Subject: certificate.ServerName, 290 Data: string(certificate.CertData), 291 } 292 tlsCertificates = append(tlsCertificates, tlsCertificate) 293 } else if tlsCertificate.Data != string(certificate.CertData) { 294 // We have made sure the upsert flag was set above. Now just figure out 295 // again if we have to actually update the data in the existing cert. 296 tlsCertificate.Data = string(certificate.CertData) 297 upserted = true 298 } 299 300 if newEntry || upserted { 301 // We append the certificate for every PEM entry in the request, so the 302 // caller knows that we processed each single item. 303 for _, entry := range pemCreated { 304 created = append(created, appsv1.RepositoryCertificate{ 305 ServerName: certificate.ServerName, 306 CertType: "https", 307 CertData: []byte(entry), 308 }) 309 } 310 saveTLSData = true 311 } 312 default: 313 // Invalid/unknown certificate type 314 return nil, fmt.Errorf("unknown certificate type: %s", certificate.CertType) 315 } 316 } 317 318 if saveSSHData { 319 err = db.settingsMgr.SaveSSHKnownHostsData(ctx, knownHostsDataToStrings(sshKnownHostsList)) 320 if err != nil { 321 return nil, err 322 } 323 } 324 325 if saveTLSData { 326 err = db.settingsMgr.SaveTLSCertificateData(ctx, tlsCertificatesToMap(tlsCertificates)) 327 if err != nil { 328 return nil, err 329 } 330 } 331 332 return &appsv1.RepositoryCertificateList{Items: created}, nil 333 } 334 335 // Batch remove configured certificates according to the selector query 336 func (db *db) RemoveRepoCertificates(ctx context.Context, selector *CertificateListSelector) (*appsv1.RepositoryCertificateList, error) { 337 var ( 338 knownHostsOld []*SSHKnownHostsEntry 339 knownHostsNew []*SSHKnownHostsEntry 340 tlsCertificatesOld []*TLSCertificate 341 tlsCertificatesNew []*TLSCertificate 342 err error 343 ) 344 345 removed := &appsv1.RepositoryCertificateList{ 346 Items: make([]appsv1.RepositoryCertificate, 0), 347 } 348 349 if selector.CertType == "" || selector.CertType == "ssh" || selector.CertType == "*" { 350 knownHostsOld, err = db.getSSHKnownHostsData() 351 if err != nil { 352 return nil, err 353 } 354 knownHostsNew = make([]*SSHKnownHostsEntry, 0) 355 356 for _, entry := range knownHostsOld { 357 if matchSSHKnownHostsEntry(entry, selector) { 358 removed.Items = append(removed.Items, appsv1.RepositoryCertificate{ 359 ServerName: entry.Host, 360 CertType: "ssh", 361 CertSubType: entry.SubType, 362 CertData: []byte(entry.Data), 363 }) 364 } else { 365 knownHostsNew = append(knownHostsNew, entry) 366 } 367 } 368 } 369 370 if selector.CertType == "" || selector.CertType == "*" || selector.CertType == "https" || selector.CertType == "tls" { 371 tlsCertificatesOld, err = db.getTLSCertificateData() 372 if err != nil { 373 return nil, err 374 } 375 tlsCertificatesNew = make([]*TLSCertificate, 0) 376 for _, entry := range tlsCertificatesOld { 377 if certutil.MatchHostName(entry.Subject, selector.HostNamePattern) { 378 // Wrap each PEM certificate into its own RepositoryCertificate object 379 // so the caller knows what has been removed actually. 380 // 381 // The downside of this is, only valid data can be removed from the CM, 382 // so if the data somehow got corrupted, it can only be removed by 383 // means of editing the CM directly using e.g. kubectl. 384 pemCertificates, err := certutil.ParseTLSCertificatesFromData(entry.Data) 385 if err != nil { 386 return nil, err 387 } 388 if len(pemCertificates) > 0 { 389 for _, pem := range pemCertificates { 390 removed.Items = append(removed.Items, appsv1.RepositoryCertificate{ 391 ServerName: entry.Subject, 392 CertType: "https", 393 CertData: []byte(pem), 394 }) 395 } 396 } 397 } else { 398 tlsCertificatesNew = append(tlsCertificatesNew, entry) 399 } 400 } 401 } 402 403 if len(knownHostsNew) < len(knownHostsOld) { 404 err = db.settingsMgr.SaveSSHKnownHostsData(ctx, knownHostsDataToStrings(knownHostsNew)) 405 if err != nil { 406 return nil, err 407 } 408 } 409 410 if len(tlsCertificatesNew) < len(tlsCertificatesOld) { 411 err = db.settingsMgr.SaveTLSCertificateData(ctx, tlsCertificatesToMap(tlsCertificatesNew)) 412 if err != nil { 413 return nil, err 414 } 415 } 416 417 return removed, nil 418 } 419 420 // Converts list of known hosts data to array of strings, suitable for storing 421 // in a known_hosts file for SSH. 422 func knownHostsDataToStrings(knownHostsList []*SSHKnownHostsEntry) []string { 423 knownHostsData := make([]string, 0) 424 for _, entry := range knownHostsList { 425 knownHostsData = append(knownHostsData, fmt.Sprintf("%s %s %s", entry.Host, entry.SubType, entry.Data)) 426 } 427 return knownHostsData 428 } 429 430 // Converts list of TLS certificates to a map whose key will be the certificate 431 // subject and the data will be a string containing TLS certificate data as PEM 432 func tlsCertificatesToMap(tlsCertificates []*TLSCertificate) map[string]string { 433 certMap := make(map[string]string) 434 for _, entry := range tlsCertificates { 435 certMap[entry.Subject] = entry.Data 436 } 437 return certMap 438 } 439 440 // Get the TLS certificate data from the config map 441 func (db *db) getTLSCertificateData() ([]*TLSCertificate, error) { 442 certificates := make([]*TLSCertificate, 0) 443 certCM, err := db.settingsMgr.GetConfigMapByName(common.ArgoCDTLSCertsConfigMapName) 444 if err != nil { 445 return nil, err 446 } 447 for key, entry := range certCM.Data { 448 certificates = append(certificates, &TLSCertificate{Subject: key, Data: entry}) 449 } 450 451 return certificates, nil 452 } 453 454 // Gets the SSH known host data from ConfigMap and parse it into an array of 455 // SSHKnownHostEntry structs. 456 func (db *db) getSSHKnownHostsData() ([]*SSHKnownHostsEntry, error) { 457 certCM, err := db.settingsMgr.GetConfigMapByName(common.ArgoCDKnownHostsConfigMapName) 458 if err != nil { 459 return nil, err 460 } 461 462 sshKnownHostsData := certCM.Data["ssh_known_hosts"] 463 entries := make([]*SSHKnownHostsEntry, 0) 464 465 // ssh_known_hosts data contains one key per line, so we must iterate over 466 // the whole data to get all keys. 467 // 468 // We validate the data found to a certain extent before we accept them as 469 // entry into our list to be returned. 470 // 471 sshKnownHostsEntries, err := certutil.ParseSSHKnownHostsFromData(sshKnownHostsData) 472 if err != nil { 473 return nil, err 474 } 475 476 for _, entry := range sshKnownHostsEntries { 477 hostname, subType, keyData, err := certutil.TokenizeSSHKnownHostsEntry(entry) 478 if err != nil { 479 return nil, err 480 } 481 entries = append(entries, &SSHKnownHostsEntry{ 482 Host: hostname, 483 SubType: subType, 484 Data: string(keyData), 485 }) 486 } 487 488 return entries, nil 489 } 490 491 func matchSSHKnownHostsEntry(entry *SSHKnownHostsEntry, selector *CertificateListSelector) bool { 492 return certutil.MatchHostName(entry.Host, selector.HostNamePattern) && (selector.CertSubType == "" || selector.CertSubType == "*" || selector.CertSubType == entry.SubType) 493 }