You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by ra...@apache.org on 2019/02/27 23:48:52 UTC

[trafficcontrol] branch master updated: Add TO deleting old DS certs on snapshot (#3333)

This is an automated email from the ASF dual-hosted git repository.

rawlin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new b6d2c0d  Add TO deleting old DS certs on snapshot (#3333)
b6d2c0d is described below

commit b6d2c0d9d7ddde7d576796b2595b6b9c960b8207
Author: Robert Butts <ro...@users.noreply.github.com>
AuthorDate: Wed Feb 27 16:48:47 2019 -0700

    Add TO deleting old DS certs on snapshot (#3333)
    
    * Add TO deleting old DS certs on snapshot
    
    * fix select race
    
    * Add docs for snapshot deleting old certs
---
 CHANGELOG.md                                       |   1 +
 docs/source/admin/traffic_ops/using.rst            |   1 +
 docs/source/api/cdns_id_snapshot.rst               |   2 +
 docs/source/api/snapshot_name.rst                  |   2 +
 .../traffic_ops_golang/cdn/dnssecrefresh.go        |   4 +
 traffic_ops/traffic_ops_golang/crconfig/handler.go |  32 +++-
 .../traffic_ops_golang/dbhelpers/db_helpers.go     |  60 ++++++
 .../deliveryservice/deleteoldcerts.go              | 203 +++++++++++++++++++++
 .../traffic_ops_golang/deliveryservice/keys.go     |   6 +-
 traffic_ops/traffic_ops_golang/riaksvc/dsutil.go   |  54 +++++-
 .../traffic_ops_golang/riaksvc/riak_services.go    |  18 +-
 11 files changed, 370 insertions(+), 13 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43455c4..4e4bd37 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - Adds the DS Record text to the cdn dnsseckeys endpoint in 1.4.
 - Added monitoring.json snapshotting. This stores the monitoring json in the same table as the crconfig snapshot. Snapshotting is now required in order to push out monitoring changes.
 - To traffic_ops_ort.pl added the ability to handle ##OVERRIDE## delivery service ANY_MAP raw remap text to replace and comment out a base delivery service remap rules. THIS IS A TEMPORARY HACK until versioned delivery services are implemented.
+- Snapshotting the CRConfig now deletes HTTPS certificates in Riak for delivery services which have been deleted in Traffic Ops.
 
 ### Changed
 - Traffic Ops Golang Endpoints
diff --git a/docs/source/admin/traffic_ops/using.rst b/docs/source/admin/traffic_ops/using.rst
index 8bc38ef..8fd38c4 100644
--- a/docs/source/admin/traffic_ops/using.rst
+++ b/docs/source/admin/traffic_ops/using.rst
@@ -1202,6 +1202,7 @@ To create a new snapshot, use the *Tools > Snapshot CRConfig* menu:
 	#. When the This will push out a new CRConfig.json. Are you sure? window opens, click **OK**.
 	#. The "Successfully wrote CRConfig.json!" window opens, click **OK**.
 
+.. Note:: Snapshotting the CDN also deletes all HTTPS certificates for every :term:`Delivery Service` which has been deleted since the last :term:`Snapshot`.
 
 .. index::
 	Invalidate Content
diff --git a/docs/source/api/cdns_id_snapshot.rst b/docs/source/api/cdns_id_snapshot.rst
index fa140a9..b7076de 100644
--- a/docs/source/api/cdns_id_snapshot.rst
+++ b/docs/source/api/cdns_id_snapshot.rst
@@ -25,6 +25,8 @@
 =======
 Performs a CDN snapshot. Effectively, this propagates the new *configuration* of the CDN to its *operating state*, which replaces the output of the :ref:`to-api-cdns-name-snapshot` endpoint with the output of the :ref:`to-api-cdns-name-snapshot-new` endpoint.
 
+.. Note:: Snapshotting the CDN also deletes all HTTPS certificates for every :term:`Delivery Service` which has been deleted since the last CDN :term:`Snapshot`.
+
 :Auth. Required: Yes
 :Roles Required: "admin" or "operations"
 :Response Type:  ``undefined``
diff --git a/docs/source/api/snapshot_name.rst b/docs/source/api/snapshot_name.rst
index d47c583..0141bf1 100644
--- a/docs/source/api/snapshot_name.rst
+++ b/docs/source/api/snapshot_name.rst
@@ -23,6 +23,8 @@
 =======
 Performs a CDN snapshot. Effectively, this propagates the new *configuration* of the CDN to its *operating state*, which replaces the output of the :ref:`to-api-cdns-name-snapshot` endpoint with the output of the :ref:`to-api-cdns-name-snapshot-new` endpoint.
 
+.. Note:: Snapshotting the CDN also deletes all HTTPS certificates for every :term:`Delivery Service` which has been deleted since the last :term:`Snapshot`.
+
 :Auth. Required: Yes
 :Roles Required: "admin" or "operations"
 :Response Type:  ``undefined``
diff --git a/traffic_ops/traffic_ops_golang/cdn/dnssecrefresh.go b/traffic_ops/traffic_ops_golang/cdn/dnssecrefresh.go
index 6eefeb8..d8ae61a 100644
--- a/traffic_ops/traffic_ops_golang/cdn/dnssecrefresh.go
+++ b/traffic_ops/traffic_ops_golang/cdn/dnssecrefresh.go
@@ -45,17 +45,21 @@ func RefreshDNSSECKeys(w http.ResponseWriter, r *http.Request) {
 		noTx := (*sql.Tx)(nil) // make a variable instead of passing nil directly, to reduce copy-paste errors
 		if err != nil {
 			api.HandleErr(w, r, noTx, http.StatusInternalServerError, nil, errors.New("RefresHDNSSECKeys getting db from context: "+err.Error()))
+			unsetInDNSSECKeyRefresh()
 			return
 		}
 		cfg, err := api.GetConfig(r.Context())
 		if err != nil {
 			api.HandleErr(w, r, noTx, http.StatusInternalServerError, nil, errors.New("RefresHDNSSECKeys getting config from context: "+err.Error()))
+			unsetInDNSSECKeyRefresh()
 			return
 		}
 
 		tx, err := db.Begin()
 		if err != nil {
 			api.HandleErr(w, r, noTx, http.StatusInternalServerError, nil, errors.New("RefresHDNSSECKeys beginning tx: "+err.Error()))
+			unsetInDNSSECKeyRefresh()
+			return
 		}
 		go doDNSSECKeyRefresh(tx, cfg) // doDNSSECKeyRefresh takes ownership of tx and MUST close it.
 	} else {
diff --git a/traffic_ops/traffic_ops_golang/crconfig/handler.go b/traffic_ops/traffic_ops_golang/crconfig/handler.go
index 99c66b3..6797128 100644
--- a/traffic_ops/traffic_ops_golang/crconfig/handler.go
+++ b/traffic_ops/traffic_ops_golang/crconfig/handler.go
@@ -29,6 +29,7 @@ import (
 	"github.com/apache/trafficcontrol/lib/go-log"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/monitoring"
 )
 
@@ -127,6 +128,12 @@ func SnapshotHandler(w http.ResponseWriter, r *http.Request) {
 	}
 	defer inf.Close()
 
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("SnapshotHandler getting db from context: "+err.Error()))
+		return
+	}
+
 	cdn, ok := inf.Params["cdn"]
 	if !ok {
 		id, ok := inf.IntParams["id"]
@@ -163,6 +170,11 @@ func SnapshotHandler(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	if err := deliveryservice.DeleteOldCerts(db.DB, inf.Tx.Tx, inf.Config, tc.CDNName(cdn)); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New(r.RemoteAddr+" snapshotting CRConfig and Monitoring: starting old certificate deletion job: "+err.Error()))
+		return
+	}
+
 	api.CreateChangeLogRawTx(api.ApiChange, "Snapshot of CRConfig and Monitor performed for "+cdn, inf.User, inf.Tx.Tx)
 	api.WriteResp(w, r, "SUCCESS")
 }
@@ -176,13 +188,21 @@ func SnapshotOldGUIHandler(w http.ResponseWriter, r *http.Request) {
 	}
 	defer inf.Close()
 
-	crConfig, err := Make(inf.Tx.Tx, inf.Params["cdn"], inf.User.UserName, r.Host, r.URL.Path, inf.Config.Version, inf.Config.CRConfigUseRequestHost, inf.Config.CRConfigEmulateOldPath)
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("SnapshotHandler getting db from context: "+err.Error()))
+		return
+	}
+
+	cdn := inf.Params["cdn"]
+
+	crConfig, err := Make(inf.Tx.Tx, cdn, inf.User.UserName, r.Host, r.URL.Path, inf.Config.Version, inf.Config.CRConfigUseRequestHost, inf.Config.CRConfigEmulateOldPath)
 	if err != nil {
 		writePerlHTMLErr(w, r, inf.Tx.Tx, errors.New(r.RemoteAddr+" making CRConfig: "+err.Error()), err)
 		return
 	}
 
-	tm, err := monitoring.GetMonitoringJSON(inf.Tx.Tx, inf.Params["cdn"])
+	tm, err := monitoring.GetMonitoringJSON(inf.Tx.Tx, cdn)
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New(r.RemoteAddr+" getting monitoring.json data: "+err.Error()))
 		return
@@ -192,7 +212,13 @@ func SnapshotOldGUIHandler(w http.ResponseWriter, r *http.Request) {
 		writePerlHTMLErr(w, r, inf.Tx.Tx, errors.New(r.RemoteAddr+" making CRConfig: "+err.Error()), err)
 		return
 	}
-	api.CreateChangeLogRawTx(api.ApiChange, "Snapshot of CRConfig performed for "+inf.Params["cdn"], inf.User, inf.Tx.Tx)
+
+	if err := deliveryservice.DeleteOldCerts(db.DB, inf.Tx.Tx, inf.Config, tc.CDNName(cdn)); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New(r.RemoteAddr+" old snapshotting CRConfig and Monitoring: starting old certificate deletion job: "+err.Error()))
+		return
+	}
+
+	api.CreateChangeLogRawTx(api.ApiChange, "Snapshot of CRConfig performed for "+cdn, inf.User, inf.Tx.Tx)
 	http.Redirect(w, r, "/tools/flash_and_close/"+url.PathEscape("Successfully wrote the CRConfig.json!"), http.StatusFound)
 }
 
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index 4f15090..1ce496f 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
@@ -116,6 +116,28 @@ func AddTenancyCheck(where string, queryValues map[string]interface{}, tenantCol
 	return where, queryValues
 }
 
+// CommitIf commits if doCommit is true at the time of execution.
+// This is designed as a defer helper.
+//
+// Example:
+//
+//  tx, err := db.Begin()
+//  txCommit := false
+//  defer dbhelpers.CommitIf(tx, &txCommit)
+//  if err := tx.Exec("select ..."); err != nil {
+//    return errors.New("executing: " + err.Error())
+//  }
+//  txCommit = true
+//  return nil
+//
+func CommitIf(tx *sql.Tx, doCommit *bool) {
+	if *doCommit {
+		tx.Commit()
+	} else {
+		tx.Rollback()
+	}
+}
+
 // GetPrivLevelFromRoleID returns the priv_level associated with a role, whether it exists, and any error.
 // This method exists on a temporary basis. After priv_level is fully deprecated and capabilities take over,
 // this method will not only no longer be needed, but the corresponding new privilege check should be done
@@ -207,3 +229,41 @@ func GetCDNDomainFromName(tx *sql.Tx, cdnName tc.CDNName) (string, bool, error)
 	}
 	return domain, true, nil
 }
+
+func GetCDNDSes(tx *sql.Tx, cdn tc.CDNName) (map[tc.DeliveryServiceName]struct{}, error) {
+	dses := map[tc.DeliveryServiceName]struct{}{}
+	qry := `SELECT xml_id from deliveryservice where cdn_id = (select id from cdn where name = $1)`
+	rows, err := tx.Query(qry, cdn)
+	if err != nil {
+		return nil, errors.New("querying: " + err.Error())
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		ds := tc.DeliveryServiceName("")
+		if err := rows.Scan(&ds); err != nil {
+			return nil, errors.New("scanning: " + err.Error())
+		}
+		dses[ds] = struct{}{}
+	}
+	return dses, nil
+}
+
+func GetCDNs(tx *sql.Tx) (map[tc.CDNName]struct{}, error) {
+	cdns := map[tc.CDNName]struct{}{}
+	qry := `SELECT name from cdn;`
+	rows, err := tx.Query(qry)
+	if err != nil {
+		return nil, errors.New("querying: " + err.Error())
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		cdn := tc.CDNName("")
+		if err := rows.Scan(&cdn); err != nil {
+			return nil, errors.New("scanning: " + err.Error())
+		}
+		cdns[cdn] = struct{}{}
+	}
+	return cdns, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/deleteoldcerts.go b/traffic_ops/traffic_ops_golang/deliveryservice/deleteoldcerts.go
new file mode 100644
index 0000000..9447908
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/deleteoldcerts.go
@@ -0,0 +1,203 @@
+package deliveryservice
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"context"
+	"database/sql"
+	"errors"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-tc"
+	"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/riaksvc"
+
+	"github.com/basho/riak-go-client"
+)
+
+// DeleteOldCerts asynchronously deletes HTTPS certificates in Riak which have no corresponding delivery service in the database.
+//
+// Note the delivery service may still be in the CRConfig! Therefore, this should only be called immediately after a CRConfig Snapshot.
+//
+// This creates a goroutine, and immediately returns. It returns an error if there was an error preparing the delete routine, such as an error creating a db transaction.
+//
+// Note because it is asynchronous, this may return a nil error, but the asynchronous goroutine may error when fetching or deleting the certificates. If such an error occurs, it will be logged to the error log.
+//
+// If certificate deletion is already being processed by a goroutine, another delete will be queued, and this immediately returns nil. Only one delete will ever be queued.
+//
+func DeleteOldCerts(db *sql.DB, tx *sql.Tx, cfg *config.Config, cdn tc.CDNName) error {
+	if !cfg.RiakEnabled {
+		log.Infoln("deleting old delivery service certificates: Riak is not enabled, returning without cleaning up old certificates.")
+		return nil
+	}
+	if db == nil {
+		return errors.New("nil db")
+	}
+	if cfg == nil {
+		return errors.New("nil config")
+	}
+	startOldCertDeleter(db, tx, time.Duration(cfg.DBQueryTimeoutSeconds)*time.Second, cfg.RiakAuthOptions, cfg.RiakPort, cdn)
+	cleanupOldCertDeleters(tx)
+	return nil
+}
+
+// deleteOldDSCerts deletes the HTTPS certificates in Riak of delivery services which have been deleted in Traffic Ops.
+func deleteOldDSCerts(tx *sql.Tx, authOpts *riak.AuthOptions, riakPort *uint, cdn tc.CDNName) error {
+	dsKeys, err := riaksvc.GetCDNSSLKeysDSNames(tx, authOpts, riakPort, cdn)
+	if err != nil {
+		return errors.New("getting riak ds keys: " + err.Error())
+	}
+
+	dses, err := dbhelpers.GetCDNDSes(tx, cdn)
+	if err != nil {
+		return errors.New("getting ds names: " + err.Error())
+	}
+
+	successes := []string{}
+	failures := []string{}
+	for ds, riakKeys := range dsKeys {
+		if _, ok := dses[ds]; ok {
+			continue
+		}
+		for _, riakKey := range riakKeys {
+			err := riaksvc.DeleteDeliveryServicesSSLKey(tx, authOpts, riakPort, riakKey)
+			if err != nil {
+				log.Errorln("deleting Riak SSL keys for Delivery Service '" + string(ds) + "' key '" + riakKey + "': " + err.Error())
+				failures = append(failures, string(ds))
+			} else {
+				log.Infoln("Deleted Riak SSL keys for delivery service which has been deleted in the database '" + string(ds) + "' key '" + riakKey + "'")
+				successes = append(successes, string(ds))
+			}
+		}
+	}
+	if len(failures) > 0 {
+		return errors.New("successfully deleted Riak SSL keys for deleted dses [" + strings.Join(successes, ", ") + "], but failed to delete Riak SSL keys for [" + strings.Join(failures, ", ") + "]; see the error log for details")
+	}
+	return nil
+}
+
+// deleteOldDSCertsDB takes a db, and creates a transaction to pass to deleteOldDSCerts.
+func deleteOldDSCertsDB(db *sql.DB, dbTimeout time.Duration, riakOpts *riak.AuthOptions, riakPort *uint, cdn tc.CDNName) {
+	dbCtx, cancelTx := context.WithTimeout(context.Background(), dbTimeout)
+	tx, err := db.BeginTx(dbCtx, nil)
+	if err != nil {
+		log.Errorln("Old Cert Deleter Job: beginning tx: " + err.Error())
+		return
+	}
+	defer cancelTx()
+	txCommit := false
+	defer dbhelpers.CommitIf(tx, &txCommit)
+	if err := deleteOldDSCerts(tx, riakOpts, riakPort, cdn); err != nil {
+		log.Errorln("deleting old DS certificates: " + err.Error())
+		return
+	}
+	txCommit = true
+}
+
+var oldCertDeleters = OldCertDeleters{D: map[tc.CDNName]*OldCertDeleter{}}
+
+type OldCertDeleters struct {
+	D map[tc.CDNName]*OldCertDeleter
+	M sync.Mutex
+}
+
+// startOldCertDeleter tells the old cert deleter goroutine to start another delete job, creating the goroutine if it doesn't exist.
+func startOldCertDeleter(db *sql.DB, tx *sql.Tx, dbTimeout time.Duration, riakOpts *riak.AuthOptions, riakPort *uint, cdn tc.CDNName) {
+	oldCertDeleter := getOrCreateOldCertDeleter(cdn)
+	oldCertDeleter.Once.Do(func() {
+		go doOldCertDeleter(oldCertDeleter.Start, oldCertDeleter.Die, db, dbTimeout, riakOpts, riakPort, cdn)
+	})
+
+	select {
+	case oldCertDeleter.Start <- struct{}{}:
+	default:
+	}
+}
+
+func getOrCreateOldCertDeleter(cdn tc.CDNName) *OldCertDeleter {
+	oldCertDeleters.M.Lock()
+	defer oldCertDeleters.M.Unlock()
+	oldCertDeleter, ok := oldCertDeleters.D[cdn]
+	if !ok {
+		oldCertDeleter = newOldCertDeleter()
+		oldCertDeleters.D[cdn] = oldCertDeleter
+	}
+	return oldCertDeleter
+}
+
+// cleanupOldCertDeleters stops all cert deleter goroutines for CDNs which have been deleted in the database.
+// Any error is logged, but not returned.
+// This is designed to be called when starting a new cert deleter job, to clean up any old cert deleters from deleted CDNs.
+// This should only be called immediately when snapshotting, and immediately after startOldCertDeleter, because otherwise a cert deleter may be removed before it can delete old certs for a given CDN.
+func cleanupOldCertDeleters(tx *sql.Tx) {
+	cdns, err := dbhelpers.GetCDNs(tx) // (map[tc.CDNName]struct{}, error) {
+	if err != nil {
+		log.Errorln("cleanupOldCertDeleters: getting cdns: " + err.Error())
+		return
+	}
+
+	oldCertDeleters.M.Lock()
+	defer oldCertDeleters.M.Unlock()
+
+	for cdn, oldCertDeleter := range oldCertDeleters.D {
+		if _, ok := cdns[cdn]; ok {
+			continue
+		}
+		delete(oldCertDeleters.D, cdn)
+		select {
+		case oldCertDeleter.Die <- struct{}{}:
+		default:
+		}
+	}
+}
+
+type OldCertDeleter struct {
+	Start chan struct{}
+	Die   chan struct{}
+	Once  sync.Once
+}
+
+func newOldCertDeleter() *OldCertDeleter {
+	return &OldCertDeleter{
+		Start: make(chan struct{}, 1),
+		Die:   make(chan struct{}, 1),
+	}
+}
+
+func doOldCertDeleter(do chan struct{}, die chan struct{}, db *sql.DB, dbTimeout time.Duration, riakOpts *riak.AuthOptions, riakPort *uint, cdn tc.CDNName) {
+	for {
+		select {
+		case <-do:
+			deleteOldDSCertsDB(db, dbTimeout, riakOpts, riakPort, cdn)
+		case <-die:
+			// Go selects aren't ordered, so double-check the do chan in case a race happened and a job came in at the same time as the die.
+			select {
+			case <-do:
+				deleteOldDSCertsDB(db, dbTimeout, riakOpts, riakPort, cdn)
+			default:
+			}
+			return
+		}
+	}
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/keys.go b/traffic_ops/traffic_ops_golang/deliveryservice/keys.go
index 365bbcb..7a82014 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/keys.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/keys.go
@@ -330,10 +330,10 @@ func verifyCertificate(certificate string, rootCA string) (string, bool, error)
 		block := &pem.Block{Type: "CERTIFICATE", Bytes: link.Raw}
 		pemEncodedChain += string(pem.EncodeToMemory(block))
 	}
-   
-  	if len(pemEncodedChain) < 1 {
+
+	if len(pemEncodedChain) < 1 {
 		return "", false, errors.New("Invalid empty certicate chain in request")
-  	}
+	}
 
 	return pemEncodedChain, false, nil
 }
diff --git a/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go b/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go
index 730e5b1..b99f0c8 100644
--- a/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go
+++ b/traffic_ops/traffic_ops_golang/riaksvc/dsutil.go
@@ -200,6 +200,19 @@ func DeleteDSSSLKeys(tx *sql.Tx, authOpts *riak.AuthOptions, riakPort *uint, xml
 	return err
 }
 
+// DeleteDeliveryServicesSSLKey deletes a Delivery Service SSL key.
+// This should almost never be used directly, prefer DeleteDSSSLKeys instead.
+// This should only be used to delete keys, which may not conform to the MakeDSSSLKeyKey format. For example when deleting all keys on a delivery service, and some may have been created manually outside Traffic Ops, or are otherwise malformed.
+func DeleteDeliveryServicesSSLKey(tx *sql.Tx, authOpts *riak.AuthOptions, riakPort *uint, key string) error {
+	err := WithCluster(tx, authOpts, riakPort, func(cluster StorageCluster) error {
+		if err := DeleteObject(key, DeliveryServiceSSLKeysBucket, cluster); err != nil {
+			return errors.New("deleting SSL keys: " + err.Error())
+		}
+		return nil
+	})
+	return err
+}
+
 // GetURLSigConfigFileName returns the filename of the Apache Traffic Server URLSig config file
 // TODO move to ats config directory/file
 func GetURLSigConfigFileName(ds tc.DeliveryServiceName) string {
@@ -260,7 +273,8 @@ func GetCDNSSLKeysObj(tx *sql.Tx, authOpts *riak.AuthOptions, riakPort *uint, cd
 		// get the deliveryservice ssl keys by xmlID and version
 		query := `cdn:` + cdnName
 		filterQuery := `_yz_rk:*latest`
-		searchDocs, err := Search(cluster, SSLKeysIndex, query, filterQuery, CDNSSLKeysLimit)
+		fields := []string{"deliveryservice", "hostname", "certificate.crt", "certificate.key"}
+		searchDocs, err := Search(cluster, SSLKeysIndex, query, filterQuery, CDNSSLKeysLimit, fields)
 		if err != nil {
 			return errors.New("riak search error: " + err.Error())
 		}
@@ -297,3 +311,41 @@ func SearchDocsToCDNSSLKeys(docs []*riak.SearchDoc) []tc.CDNSSLKey {
 	}
 	return keys
 }
+
+// GetCDNSSLKeysDSNames returns the delivery service names (xml_id) of every delivery service on the given cdn with SSL keys in Riak, and the keys currently in Riak.
+// Returns map[tc.DeliveryServiceName][]key
+func GetCDNSSLKeysDSNames(tx *sql.Tx, authOpts *riak.AuthOptions, riakPort *uint, cdn tc.CDNName) (map[tc.DeliveryServiceName][]string, error) {
+	dsVersions := map[tc.DeliveryServiceName][]string{}
+	err := WithCluster(tx, authOpts, riakPort, func(cluster StorageCluster) error {
+		// get the deliveryservice ssl keys by xmlID and version
+		query := `cdn:` + string(cdn)
+		filterQuery := ""
+		fields := []string{"_yz_rk", "deliveryservice"} // '_yz_rk' is the magic Riak field that populates the key. Without this, doc.Key would be empty.
+		searchDocs, err := Search(cluster, SSLKeysIndex, query, filterQuery, CDNSSLKeysLimit, fields)
+		if err != nil {
+			return errors.New("riak search error: " + err.Error())
+		}
+		if len(searchDocs) == 0 {
+			return nil // no error, and leave keys empty
+		}
+
+		for _, doc := range searchDocs {
+			dses := doc.Fields["deliveryservice"]
+			if len(dses) == 0 {
+				log.Errorln("Riak had a CDN '" + string(cdn) + "' key with no delivery service '" + doc.Key + "' - ignoring!")
+				continue
+			}
+			if len(dses) > 1 {
+				log.Errorf("Riak had a CDN '"+string(cdn)+"' key with multiple delivery services '"+doc.Key+"' deliveryservices '%+v' - ignoring all but the first!\n", dses)
+			}
+			ds := tc.DeliveryServiceName(dses[0])
+
+			dsVersions[ds] = append(dsVersions[ds], doc.Key)
+		}
+		return nil
+	})
+	if err != nil {
+		return nil, errors.New("with cluster error: " + err.Error())
+	}
+	return dsVersions, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/riaksvc/riak_services.go b/traffic_ops/traffic_ops_golang/riaksvc/riak_services.go
index 9b1287e..e0b5bed 100644
--- a/traffic_ops/traffic_ops_golang/riaksvc/riak_services.go
+++ b/traffic_ops/traffic_ops_golang/riaksvc/riak_services.go
@@ -196,7 +196,6 @@ func FetchObjectValues(key string, bucket string, cluster StorageCluster) ([]*ri
 	return fvc.Response.Values, nil
 }
 
-// saves an object to riak storage
 func SaveObject(obj *riak.Object, bucket string, cluster StorageCluster) error {
 	if cluster == nil {
 		return errors.New("ERROR: No valid cluster on which to execute a command")
@@ -357,13 +356,20 @@ func WithCluster(tx *sql.Tx, authOpts *riak.AuthOptions, riakPort *uint, f func(
 }
 
 // Search searches Riak for the given query. Returns nil and a nil error if no object was found.
-func Search(cluster StorageCluster, index string, query string, filterQuery string, numRows int) ([]*riak.SearchDoc, error) {
-	iCmd, err := riak.NewSearchCommandBuilder().
+// If fields is empty, all fields will be returned.
+func Search(cluster StorageCluster, index string, query string, filterQuery string, numRows int, fields []string) ([]*riak.SearchDoc, error) {
+	riakCmd := riak.NewSearchCommandBuilder().
 		WithIndexName(index).
 		WithQuery(query).
-		WithFilterQuery(filterQuery).
-		WithNumRows(uint32(numRows)).
-		Build()
+		WithNumRows(uint32(numRows))
+	if len(filterQuery) > 0 {
+		riakCmd = riakCmd.WithFilterQuery(filterQuery)
+	}
+	if len(fields) > 0 {
+		riakCmd = riakCmd.WithReturnFields(fields...)
+	}
+	iCmd, err := riakCmd.Build()
+
 	if err != nil {
 		return nil, errors.New("building Riak command: " + err.Error())
 	}