blob: ea6108a319ffb67286251a7c951bd27b61e8400f [file] [log] [blame]
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"
"net/http"
"strconv"
"time"
"github.com/apache/trafficcontrol/lib/go-log"
"github.com/apache/trafficcontrol/lib/go-rfc"
"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/auth"
"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/trafficvault"
)
type DsKey struct {
XmlId string
Version sql.NullInt64
}
type DsExpirationInfo struct {
XmlId string
Version util.JSONIntStr
Expiration time.Time
AuthType string
Error error
}
type ExpirationSummary struct {
LetsEncryptExpirations []DsExpirationInfo
SelfSignedExpirations []DsExpirationInfo
AcmeExpirations []DsExpirationInfo
OtherExpirations []DsExpirationInfo
}
const emailTemplateFile = "/opt/traffic_ops/app/templates/send_mail/autorenewcerts_mail.html"
const API_ACME_AUTORENEW = "acme_autorenew"
// RenewCertificatesDeprecated renews all SSL certificates that are expiring within a certain time limit with a deprecation alert.
//// This will renew Let's Encrypt and ACME certificates.
func RenewCertificatesDeprecated(w http.ResponseWriter, r *http.Request) {
renewCertificates(w, r, true)
}
// RenewCertificates renews all SSL certificates that are expiring within a certain time limit.
// This will renew Let's Encrypt and ACME certificates.
func RenewCertificates(w http.ResponseWriter, r *http.Request) {
renewCertificates(w, r, false)
}
func renewCertificates(w http.ResponseWriter, r *http.Request, deprecated bool) {
deprecation := util.StrPtr(API_ACME_AUTORENEW)
inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
if userErr != nil || sysErr != nil {
api.HandleErrOptionalDeprecation(w, r, inf.Tx.Tx, errCode, userErr, sysErr, deprecated, deprecation)
return
}
defer inf.Close()
if !inf.Config.TrafficVaultEnabled {
api.HandleErrOptionalDeprecation(w, r, inf.Tx.Tx, http.StatusInternalServerError, errors.New("the Traffic Vault service is unavailable"), errors.New("getting SSL keys from Traffic Vault by xml id: Traffic Vault is not configured"), deprecated, deprecation)
return
}
rows, err := inf.Tx.Tx.Query(`SELECT xml_id, ssl_key_version, cdn_id FROM deliveryservice WHERE ssl_key_version != 0`)
if err != nil {
api.HandleErrOptionalDeprecation(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err, deprecated, deprecation)
return
}
defer rows.Close()
existingCerts := []ExistingCerts{}
cdnMap := make(map[int]bool)
cdns := []int{}
var cdn int
for rows.Next() {
ds := DsKey{}
err := rows.Scan(&ds.XmlId, &ds.Version, &cdn)
if err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
return
}
cdnMap[cdn] = true
existingCerts = append(existingCerts, ExistingCerts{Version: ds.Version, XmlId: ds.XmlId})
}
for k, _ := range cdnMap {
cdns = append(cdns, k)
}
userErr, sysErr, statusCode := dbhelpers.CheckIfCurrentUserCanModifyCDNsByID(inf.Tx.Tx, cdns, inf.User.UserName)
if userErr != nil || sysErr != nil {
api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr)
return
}
ctx, cancelTx := context.WithTimeout(r.Context(), AcmeTimeout*time.Duration(len(existingCerts)))
asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(inf.Tx.Tx, "ACME async job has started.")
if userErr != nil || sysErr != nil {
defer cancelTx()
api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
return
}
go RunAutorenewal(existingCerts, inf.Config, ctx, cancelTx, inf.User, asyncStatusId, inf.Vault)
var alerts tc.Alerts
if deprecated {
alerts.AddAlerts(api.CreateDeprecationAlerts(deprecation))
}
alerts.AddAlert(tc.Alert{
Text: "Beginning async call to renew certificates. This may take a few minutes. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId),
Level: tc.SuccessLevel.String(),
})
w.Header().Add(rfc.Location, api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
api.WriteAlerts(w, r, http.StatusAccepted, alerts)
}
func RunAutorenewal(existingCerts []ExistingCerts, cfg *config.Config, ctx context.Context, cancelTx context.CancelFunc, currentUser *auth.CurrentUser, asyncStatusId int, tv trafficvault.TrafficVault) {
defer cancelTx()
db, err := api.GetDB(ctx)
if err != nil {
log.Errorf("Error getting db: %s", err.Error())
if asycErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "ACME renewal failed.", asyncStatusId, true); asycErr != nil {
log.Errorf("updating async status for id %v: %v", asyncStatusId, asycErr)
}
return
}
tx, err := db.Begin()
if err != nil {
log.Errorf("Error getting tx: %s", err.Error())
if asycErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "ACME renewal failed.", asyncStatusId, true); asycErr != nil {
log.Errorf("updating async status for id %v: %v", asyncStatusId, asycErr)
}
return
}
defer tx.Commit()
logTx, err := db.Begin()
if err != nil {
log.Errorf("Error getting logTx: %s", err.Error())
if asycErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "ACME renewal failed.", asyncStatusId, true); asycErr != nil {
log.Errorf("updating async status for id %v: %v", asyncStatusId, asycErr)
}
return
}
defer logTx.Commit()
keysFound := ExpirationSummary{}
renewedCount := 0
errorCount := 0
for _, ds := range existingCerts {
if !ds.Version.Valid || ds.Version.Int64 == 0 {
continue
}
dsExpInfo := DsExpirationInfo{}
keyObj, ok, err := tv.GetDeliveryServiceSSLKeys(ds.XmlId, strconv.Itoa(int(ds.Version.Int64)), tx, ctx)
if err != nil {
log.Errorf("getting ssl keys for xmlId: %s and version: %d : %s", ds.XmlId, ds.Version.Int64, err.Error())
dsExpInfo.XmlId = ds.XmlId
dsExpInfo.Version = util.JSONIntStr(int(ds.Version.Int64))
dsExpInfo.Error = errors.New("getting ssl keys for xmlId: " + ds.XmlId + " and version: " + strconv.Itoa(int(ds.Version.Int64)) + " :" + err.Error())
keysFound.OtherExpirations = append(keysFound.OtherExpirations, dsExpInfo)
continue
}
if !ok {
log.Errorf("no object found for the specified key with xmlId: %s and version: %d", ds.XmlId, ds.Version.Int64)
dsExpInfo.XmlId = ds.XmlId
dsExpInfo.Version = util.JSONIntStr(int(ds.Version.Int64))
dsExpInfo.Error = errors.New("no object found for the specified key with xmlId: " + ds.XmlId + " and version: " + strconv.Itoa(int(ds.Version.Int64)))
keysFound.OtherExpirations = append(keysFound.OtherExpirations, dsExpInfo)
continue
}
err = Base64DecodeCertificate(&keyObj.Certificate)
if err != nil {
log.Errorf("cert autorenewal: error getting SSL keys for XMLID '%s': %s", ds.XmlId, err.Error())
dsExpInfo.XmlId = ds.XmlId
dsExpInfo.Version = util.JSONIntStr(int(ds.Version.Int64))
dsExpInfo.Error = errors.New("decoding the certificate for xmlId: " + ds.XmlId + " and version: " + strconv.Itoa(int(ds.Version.Int64)))
keysFound.OtherExpirations = append(keysFound.OtherExpirations, dsExpInfo)
continue
}
expiration, _, err := ParseExpirationAndSansFromCert([]byte(keyObj.Certificate.Crt), keyObj.Hostname)
if err != nil {
log.Errorf("cert autorenewal: %s: %s", ds.XmlId, err.Error())
dsExpInfo.XmlId = ds.XmlId
dsExpInfo.Version = util.JSONIntStr(int(ds.Version.Int64))
dsExpInfo.Error = errors.New("parsing the expiration for xmlId: " + ds.XmlId + " and version: " + strconv.Itoa(int(ds.Version.Int64)))
keysFound.OtherExpirations = append(keysFound.OtherExpirations, dsExpInfo)
continue
}
// Renew only certificates within configured limit. Default is 30 days.
if cfg.ConfigAcmeRenewal.RenewDaysBeforeExpiration == 0 {
cfg.ConfigAcmeRenewal.RenewDaysBeforeExpiration = 30
}
if expiration.After(time.Now().Add(time.Hour * 24 * time.Duration(cfg.ConfigAcmeRenewal.RenewDaysBeforeExpiration))) {
continue
}
log.Debugf("renewing certificate for xmlId = %s, version = %d, and auth type = %s ", ds.XmlId, ds.Version.Int64, keyObj.AuthType)
newVersion := util.JSONIntStr(keyObj.Version.ToInt64() + 1)
dsExpInfo.XmlId = keyObj.DeliveryService
dsExpInfo.Version = keyObj.Version
dsExpInfo.Expiration = expiration
dsExpInfo.AuthType = keyObj.AuthType
if keyObj.AuthType == tc.LetsEncryptAuthType || (keyObj.AuthType == tc.SelfSignedCertAuthType && cfg.ConfigLetsEncrypt.ConvertSelfSigned) {
req := tc.DeliveryServiceAcmeSSLKeysReq{
DeliveryServiceSSLKeysReq: tc.DeliveryServiceSSLKeysReq{
HostName: &keyObj.Hostname,
DeliveryService: &keyObj.DeliveryService,
CDN: &keyObj.CDN,
Version: &newVersion,
AuthType: &keyObj.AuthType,
Key: &keyObj.Key,
},
}
if err := GetAcmeCertificates(cfg, req, ctx, nil, false, currentUser, 0, tv); err != nil {
dsExpInfo.Error = err
errorCount++
} else {
renewedCount++
}
keysFound.LetsEncryptExpirations = append(keysFound.LetsEncryptExpirations, dsExpInfo)
} else if keyObj.AuthType == tc.SelfSignedCertAuthType {
keysFound.SelfSignedExpirations = append(keysFound.SelfSignedExpirations, dsExpInfo)
} else {
acmeAccount := GetAcmeAccountConfig(cfg, keyObj.AuthType)
if acmeAccount == nil {
keysFound.OtherExpirations = append(keysFound.OtherExpirations, dsExpInfo)
} else {
// background httpCtx since this is run in a goroutine spawned off the original http request
// so the context isn't cancelled when the http connection is closed
userErr, sysErr, statusCode := renewAcmeCerts(cfg, keyObj.DeliveryService, ctx, context.Background(), currentUser, tv)
if userErr != nil {
errorCount++
dsExpInfo.Error = userErr
} else if sysErr != nil {
errorCount++
dsExpInfo.Error = sysErr
} else if statusCode != http.StatusOK {
errorCount++
dsExpInfo.Error = errors.New("Status code not 200: " + strconv.Itoa(statusCode))
} else {
renewedCount++
}
keysFound.AcmeExpirations = append(keysFound.AcmeExpirations, dsExpInfo)
}
}
if asycErr := api.UpdateAsyncStatus(db, api.AsyncPending, "ACME renewal in progress. "+strconv.Itoa(renewedCount)+" certs renewed, "+strconv.Itoa(errorCount)+" errors.", asyncStatusId, false); asycErr != nil {
log.Errorf("updating async status for id %v: %v", asyncStatusId, asycErr)
}
}
// put status as succeeded if any certs were successfully renewed
asyncStatus := api.AsyncSucceeded
if errorCount > 0 && renewedCount == 0 {
asyncStatus = api.AsyncFailed
}
if asycErr := api.UpdateAsyncStatus(db, asyncStatus, "ACME renewal complete. "+strconv.Itoa(renewedCount)+" certs renewed, "+strconv.Itoa(errorCount)+" errors.", asyncStatusId, true); asycErr != nil {
log.Errorf("updating async status for id %v: %v", asyncStatusId, asycErr)
}
if cfg.SMTP.Enabled && cfg.ConfigAcmeRenewal.SummaryEmail != "" {
errCode, userErr, sysErr := AlertExpiringCerts(keysFound, *cfg)
if userErr != nil || sysErr != nil {
log.Errorf("cert autorenewal: sending email: errCode: %d userErr: %v sysErr: %v", errCode, userErr, sysErr)
return
}
}
}
func AlertExpiringCerts(certsFound ExpirationSummary, config config.Config) (int, error, error) {
header := "From: " + config.ConfigTO.EmailFrom.String() + "\r\n" +
"To: " + config.ConfigAcmeRenewal.SummaryEmail + "\r\n" +
"MIME-version: 1.0;\r\n" +
"Content-Type: text/html; charset=\"UTF-8\";\r\n" +
"Subject: Certificate Expiration Summary\r\n\r\n"
return api.SendEmailFromTemplate(config, header, certsFound, emailTemplateFile, config.ConfigAcmeRenewal.SummaryEmail)
}
type ExistingCerts struct {
Version sql.NullInt64
XmlId string
}