diff --git a/CHANGELOG.md b/CHANGELOG.md index 88afdf08a9..50ecd074fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ### Fixed - Fixed Traffic Router crs/stats to prevent overflow and to correctly record the time used in averages. +- [#5893](https://github.com/apache/trafficcontrol/issues/5893) - A self signed certificate is created when an HTTPS delivery service is created or an HTTP delivery service is updated to HTTPS. ### Changed - Updated `t3c` to request less unnecessary deliveryservice-server assignment and invalidation jobs data via new query params supported by Traffic Ops diff --git a/docs/source/admin/traffic_ops.rst b/docs/source/admin/traffic_ops.rst index 2d4af77a37..e4b57655e9 100644 --- a/docs/source/admin/traffic_ops.rst +++ b/docs/source/admin/traffic_ops.rst @@ -315,6 +315,14 @@ This file deals with the configuration parameters of running Traffic Ops itself. :renew_days_before_expiration: Set the number of days before expiration date to renew certificates. :summary_email: The email address to use for summarizing certificate expiration and renewal status. If it is blank, no email will be sent. +:default_certificate_info: This is an optional object to define default values when generating a self signed certificate when an HTTPS delivery service is created or updated. If this is an empty object or not present in the :ref:`cdn.conf` then the term "Placeholder" will be used for all fields. + + :business_unit: An optional field which, if present, will represent the business unit for which the SSL certificate was generated + :city: An optional field which, if present, will represent the resident city of the generated SSL certificate + :organization: An optional field which, if present, will represent the organization for which the SSL certificate was generated + :country: An optional field which, if present, will represent the resident country of the generated SSL certificate + :state: An optional field which, if present, will represent the resident state or province of the generated SSL certificate + :geniso: This object contains configuration options for system ISO generation. :iso_root_path: Sets the filesystem path to the root of the ISO generation directory. For default installations, this should usually be set to :file:`/opt/traffic_ops/app/public`. diff --git a/traffic_ops/app/conf/cdn.conf b/traffic_ops/app/conf/cdn.conf index b46bf81f37..f44c4a4689 100644 --- a/traffic_ops/app/conf/cdn.conf +++ b/traffic_ops/app/conf/cdn.conf @@ -90,5 +90,12 @@ "kid" : "", "hmac_encoded" : "" } - ] + ], + "default_certificate_info" : { + "business_unit" : "", + "city" : "", + "organization" : "", + "country" : "", + "state" : "" + } } diff --git a/traffic_ops/testing/api/v4/deliveryservices_test.go b/traffic_ops/testing/api/v4/deliveryservices_test.go index d3187c8002..09fdef15c2 100644 --- a/traffic_ops/testing/api/v4/deliveryservices_test.go +++ b/traffic_ops/testing/api/v4/deliveryservices_test.go @@ -42,6 +42,7 @@ func TestDeliveryServices(t *testing.T) { header.Set(rfc.IfModifiedSince, ti) header.Set(rfc.IfUnmodifiedSince, ti) if includeSystemTests { + t.Run("Verify SSL key generation on DS creation", VerifySSLKeysOnDsCreationTest) t.Run("Update CDN for a Delivery Service with SSL keys", SSLDeliveryServiceCDNUpdateTest) t.Run("Create URL Signature keys for a Delivery Service", CreateTestDeliveryServicesURLSignatureKeys) t.Run("Retrieve URL Signature keys for a Delivery Service", GetTestDeliveryServicesURLSignatureKeys) @@ -137,7 +138,7 @@ func CUDDeliveryServiceWithLocks(t *testing.T) { if len(types.Response) < 1 { t.Fatal("expected at least one type") } - customDS := getCustomDS(cdn.ID, types.Response[0].ID, "cdn_locks_test_ds_name", "routingName", "https://test_cdn_locks.com", "cdn_locks_test_ds_xml_id") + customDS := getCustomDS(cdn.ID, types.Response[0].ID, "cdn-locks-test-ds-name", "edge", "https://test-cdn-locks.com", "cdn-locks-test-ds-xml-id") // Create a lock for this user _, _, err = userSession.CreateCDNLock(tc.CDNLock{ @@ -706,6 +707,43 @@ func DeliveryServiceSSLKeys(t *testing.T) { } } +func VerifySSLKeysOnDsCreationTest(t *testing.T) { + for _, ds := range testData.DeliveryServices { + if !(*ds.Protocol == tc.DSProtocolHTTPS || *ds.Protocol == tc.DSProtocolHTTPAndHTTPS || *ds.Protocol == tc.DSProtocolHTTPToHTTPS) { + continue + } + var err error + dsSSLKey := new(tc.DeliveryServiceSSLKeys) + for tries := 0; tries < 5; tries++ { + time.Sleep(time.Second) + var sslKeysResp tc.DeliveryServiceSSLKeysResponse + sslKeysResp, _, err = TOSession.GetDeliveryServiceSSLKeys(*ds.XMLID, client.RequestOptions{}) + *dsSSLKey = sslKeysResp.Response + if err == nil && dsSSLKey != nil { + break + } + } + + if err != nil || dsSSLKey == nil { + t.Fatalf("unable to get DS %s SSL key: %v", *ds.XMLID, err) + } + if dsSSLKey.Certificate.Key == "" { + t.Errorf("expected a valid key but got nothing") + } + if dsSSLKey.Certificate.Crt == "" { + t.Errorf("expected a valid certificate, but got nothing") + } + if dsSSLKey.Certificate.CSR == "" { + t.Errorf("expected a valid CSR, but got nothing") + } + + err = deliveryservice.Base64DecodeCertificate(&dsSSLKey.Certificate) + if err != nil { + t.Fatalf("couldn't decode certificate: %v", err) + } + } +} + func SSLDeliveryServiceCDNUpdateTest(t *testing.T) { cdnNameOld := "sslkeytransfer" oldCdn := createBlankCDN(cdnNameOld, t) diff --git a/traffic_ops/traffic_ops_golang/config/config.go b/traffic_ops/traffic_ops_golang/config/config.go index af632f891b..b7be58ae24 100644 --- a/traffic_ops/traffic_ops_golang/config/config.go +++ b/traffic_ops/traffic_ops_golang/config/config.go @@ -58,8 +58,9 @@ type Config struct { InfluxEnabled bool InfluxDBConfPath string `json:"influxdb_conf_path"` Version string - UseIMS bool `json:"use_ims"` - RoleBasedPermissions bool `json:"role_based_permissions"` + UseIMS bool `json:"use_ims"` + RoleBasedPermissions bool `json:"role_based_permissions"` + DefaultCertificateInfo *DefaultCertificateInfo `json:"default_certificate_info"` } // ConfigHypnotoad carries http setting for hypnotoad (mojolicious) server @@ -173,6 +174,38 @@ type ConfigAcmeAccount struct { HmacEncoded string `json:"hmac_encoded"` } +type DefaultCertificateInfo struct { + BusinessUnit string `json:"business_unit"` + City string `json:"city"` + Organization string `json:"organization"` + Country string `json:"country"` + State string `json:"state"` +} + +func (d *DefaultCertificateInfo) Validate() (error, bool) { + missingList := []string{} + if d.BusinessUnit == "" { + missingList = append(missingList, "BusinessUnit") + } + if d.City == "" { + missingList = append(missingList, "City") + } + if d.Organization == "" { + missingList = append(missingList, "Organization") + } + if d.Country == "" { + missingList = append(missingList, "Country") + } + if d.State == "" { + missingList = append(missingList, "State") + } + + if len(missingList) != 0 { + return fmt.Errorf("default certificate information is missing: %s", missingList), false + } + return nil, true +} + // ConfigDatabase reflects the structure of the database.conf file type ConfigDatabase struct { Description string `json:"description"` diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go index f3f35e62e4..8d70fc320a 100644 --- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go +++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go @@ -1603,3 +1603,14 @@ func GetDSIDFromStaticDNSEntry(tx *sql.Tx, staticDNSEntryID int) (int, error) { } return dsID, nil } + +// GetCDNNameDomain returns the name and domain for a given CDN ID. +func GetCDNNameDomain(cdnID int, tx *sql.Tx) (string, string, error) { + q := `SELECT cdn.name, cdn.domain_name from cdn where cdn.id = $1` + cdnName := "" + cdnDomain := "" + if err := tx.QueryRow(q, cdnID).Scan(&cdnName, &cdnDomain); err != nil { + return "", "", fmt.Errorf("getting cdn name and domain for cdn '%v': "+err.Error(), cdnID) + } + return cdnName, cdnDomain, nil +} diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go index 77d71edf45..7a675fb212 100644 --- a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go +++ b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices.go @@ -219,6 +219,7 @@ func CreateV40(w http.ResponseWriter, r *http.Request) { } alerts := res.TLSVersionsAlerts() alerts.AddNewAlert(tc.SuccessLevel, "Delivery Service creation was successful") + w.Header().Set("Location", fmt.Sprintf("/api/4.0/deliveryservices?id=%d", *res.ID)) api.WriteAlertsObj(w, r, http.StatusCreated, alerts, []tc.DeliveryServiceV40{*res}) } @@ -548,6 +549,13 @@ func createV40(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, dsV40 t dsV40 = ds + if inf.Config.TrafficVaultEnabled && ds.Protocol != nil && (*ds.Protocol == tc.DSProtocolHTTPS || *ds.Protocol == tc.DSProtocolHTTPAndHTTPS || *ds.Protocol == tc.DSProtocolHTTPToHTTPS) { + err, errCode := GeneratePlaceholderSelfSignedCert(dsV40, inf, r.Context()) + if err != nil || errCode != http.StatusOK { + return nil, errCode, nil, fmt.Errorf("creating self signed default cert: %v", err) + } + } + return &dsV40, http.StatusOK, nil, nil } @@ -715,6 +723,7 @@ func UpdateV40(w http.ResponseWriter, r *http.Request) { } alerts := res.TLSVersionsAlerts() alerts.AddNewAlert(tc.SuccessLevel, "Delivery Service update was successful") + api.WriteAlertsObj(w, r, http.StatusOK, alerts, []tc.DeliveryServiceV40{*res}) } @@ -1124,6 +1133,14 @@ func updateV40(w http.ResponseWriter, r *http.Request, inf *api.APIInfo, dsV40 * } dsV40 = (*tc.DeliveryServiceV40)(&ds) + + if inf.Config.TrafficVaultEnabled && ds.Protocol != nil && (*ds.Protocol == tc.DSProtocolHTTPS || *ds.Protocol == tc.DSProtocolHTTPAndHTTPS || *ds.Protocol == tc.DSProtocolHTTPToHTTPS) { + err, errCode := GeneratePlaceholderSelfSignedCert(*dsV40, inf, r.Context()) + if err != nil || errCode != http.StatusOK { + return nil, errCode, nil, fmt.Errorf("creating self signed default cert: %v", err) + } + } + return dsV40, http.StatusOK, nil, nil } diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/sslkeys.go b/traffic_ops/traffic_ops_golang/deliveryservice/sslkeys.go index a1a7f16e2c..d1b1f2e224 100644 --- a/traffic_ops/traffic_ops_golang/deliveryservice/sslkeys.go +++ b/traffic_ops/traffic_ops_golang/deliveryservice/sslkeys.go @@ -25,9 +25,12 @@ import ( "errors" "net/http" "strconv" + "strings" "github.com/apache/trafficcontrol/lib/go-tc" + "github.com/apache/trafficcontrol/lib/go-util" "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api" + "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/config" "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers" "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant" "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/trafficvault" @@ -70,7 +73,7 @@ func GenerateSSLKeys(w http.ResponseWriter, r *http.Request) { api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr) return } - if err := generatePutRiakKeys(req, inf.Tx.Tx, inf.Vault, r.Context()); err != nil { + if err := generatePutTrafficVaultSSLKeys(req, inf.Tx.Tx, inf.Vault, r.Context()); err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("generating and putting SSL keys: "+err.Error())) return } @@ -82,9 +85,9 @@ func GenerateSSLKeys(w http.ResponseWriter, r *http.Request) { api.WriteResp(w, r, "Successfully created ssl keys for "+*req.DeliveryService) } -// generatePutRiakKeys generates a certificate, csr, and key from the given request, and insert it into the Riak key database. +// generatePutTrafficVaultSSLKeys generates a certificate, csr, and key from the given request, and insert it into the Riak key database. // The req MUST be validated, ensuring required fields exist. -func generatePutRiakKeys(req tc.DeliveryServiceGenSSLKeysReq, tx *sql.Tx, tv trafficvault.TrafficVault, ctx context.Context) error { +func generatePutTrafficVaultSSLKeys(req tc.DeliveryServiceGenSSLKeysReq, tx *sql.Tx, tv trafficvault.TrafficVault, ctx context.Context) error { dsSSLKeys := tc.DeliveryServiceSSLKeys{ CDN: *req.CDN, DeliveryService: *req.DeliveryService, @@ -110,3 +113,74 @@ func generatePutRiakKeys(req tc.DeliveryServiceGenSSLKeysReq, tx *sql.Tx, tv tra } return nil } + +// GeneratePlaceholderSelfSignedCert generates a self-signed SSL certificate as a placeholder when a new HTTPS +// delivery service is created or an HTTP delivery service is updated to use HTTPS. +func GeneratePlaceholderSelfSignedCert(ds tc.DeliveryServiceV4, inf *api.APIInfo, context context.Context) (error, int) { + tx := inf.Tx.Tx + tv := inf.Vault + _, ok, err := tv.GetDeliveryServiceSSLKeys(*ds.XMLID, "", tx, context) + if err != nil { + return errors.New("getting latest ssl keys for xmlId: " + *ds.XMLID + " : " + err.Error()), http.StatusInternalServerError + } + if ok { + return nil, http.StatusOK + } + + version := util.JSONIntStr(1) + + cdnName, cdnDomain, err := dbhelpers.GetCDNNameDomain(*ds.CDNID, tx) + if err != nil { + return err, http.StatusInternalServerError + } + + cdnNameStr := string(cdnName) + + if ds.ExampleURLs == nil { + ds.ExampleURLs = MakeExampleURLs(ds.Protocol, *ds.Type, *ds.RoutingName, *ds.MatchList, cdnDomain) + } + + hostname := strings.Split(ds.ExampleURLs[0], "://")[1] + if ds.Type.IsHTTP() { + parts := strings.Split(hostname, ".") + parts[0] = "*" + hostname = strings.Join(parts, ".") + } + + req := tc.DeliveryServiceGenSSLKeysReq{ + DeliveryServiceSSLKeysReq: tc.DeliveryServiceSSLKeysReq{ + CDN: &cdnNameStr, + DeliveryService: ds.XMLID, + HostName: &hostname, + Key: ds.XMLID, + Version: &version, + BusinessUnit: util.StrPtr("Placeholder"), + City: util.StrPtr("Placeholder"), + Organization: util.StrPtr("Placeholder"), + Country: util.StrPtr("Placeholder"), + State: util.StrPtr("Placeholder"), + }, + } + + if (inf.Config.DefaultCertificateInfo != nil && *inf.Config.DefaultCertificateInfo != config.DefaultCertificateInfo{}) { + defaultCertInfo := inf.Config.DefaultCertificateInfo + if err, ok := defaultCertInfo.Validate(); !ok { + return err, http.StatusInternalServerError + } + + req.BusinessUnit = &defaultCertInfo.BusinessUnit + req.City = &defaultCertInfo.City + req.Organization = &defaultCertInfo.Organization + req.Country = &defaultCertInfo.Country + req.State = &defaultCertInfo.State + } + + if err := generatePutTrafficVaultSSLKeys(req, tx, inf.Vault, context); err != nil { + return errors.New("generating and putting SSL keys: " + err.Error()), http.StatusInternalServerError + } + if err := updateSSLKeyVersion(*req.DeliveryService, req.Version.ToInt64(), tx); err != nil { + return errors.New("generating SSL keys for delivery service '" + *req.DeliveryService + "': " + err.Error()), http.StatusInternalServerError + } + + return nil, http.StatusOK +}