-
Notifications
You must be signed in to change notification settings - Fork 339
/
dnssecrefresh.go
504 lines (463 loc) · 18.6 KB
/
dnssecrefresh.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
package cdn
/*
* 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"
"fmt"
"net/http"
"strconv"
"sync/atomic"
"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/deliveryservice"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/trafficvault"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
)
var noTx = (*sql.Tx)(nil) // make a variable instead of passing nil directly, to reduce copy-paste errors
func RefreshDNSSECKeys(w http.ResponseWriter, r *http.Request) {
_, _, status, usrErr, sysErr := refreshDNSSECKeys(r.Context())
if usrErr != nil || sysErr != nil {
api.HandleErr(w, r, noTx, status, usrErr, sysErr)
return
}
api.WriteResp(w, r, "Checking DNSSEC keys for refresh in the background")
}
func RefreshDNSSECKeysV4(w http.ResponseWriter, r *http.Request) {
asyncStatusID, started, status, usrErr, sysErr := refreshDNSSECKeys(r.Context())
if usrErr != nil || sysErr != nil {
api.HandleErr(w, r, noTx, status, usrErr, sysErr)
return
}
message := "the server is already executing a DNSSEC refresh"
if started {
message = "Starting DNSSEC key refresh in the background. This may take a few minutes. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusID)
}
w.Header().Add(rfc.Location, api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusID))
api.WriteAlerts(w, r, http.StatusAccepted, tc.CreateAlerts(tc.SuccessLevel, message))
}
// refreshDNSSECKeys starts a goroutine to refresh the DNSSEC keys for all delivery services in all CDNs (except for the CDN KSKs which need to be refreshed separately).
// It returns the async status ID, a bool indicating whether the refresh job was started or not, an HTTP status, a user error, and a system error.
func refreshDNSSECKeys(ctx context.Context) (int, bool, int, error, error) {
if setInDNSSECKeyRefresh() {
db, err := api.GetDB(ctx)
if err != nil {
unsetInDNSSECKeyRefresh()
return 0, false, http.StatusInternalServerError, nil, errors.New("RefreshDNSSECKeys getting db from context: " + err.Error())
}
cfg, err := api.GetConfig(ctx)
if err != nil {
unsetInDNSSECKeyRefresh()
return 0, false, http.StatusInternalServerError, nil, errors.New("RefreshDNSSECKeys getting config from context: " + err.Error())
}
user, err := auth.GetCurrentUser(ctx)
if err != nil {
unsetInDNSSECKeyRefresh()
return 0, false, http.StatusInternalServerError, nil, errors.New("RefreshDNSSECKeys getting user from context: " + err.Error())
}
if !cfg.TrafficVaultEnabled {
unsetInDNSSECKeyRefresh()
return 0, false, http.StatusInternalServerError, nil, errors.New("refreshing DNSSEC keys: Traffic Vault not enabled")
}
tv, err := api.GetTrafficVault(ctx)
if err != nil {
unsetInDNSSECKeyRefresh()
return 0, false, http.StatusInternalServerError, nil, errors.New("RefreshDNSSECKeys getting Traffic Vault from context: " + err.Error())
}
tx, err := db.Begin()
if err != nil {
unsetInDNSSECKeyRefresh()
return 0, false, http.StatusInternalServerError, nil, errors.New("RefreshDNSSECKeys beginning tx: " + err.Error())
}
asyncTx, err := db.Begin()
if err != nil {
unsetInDNSSECKeyRefresh()
return 0, false, http.StatusInternalServerError, nil, errors.New("RefreshDNSSECKeys beginning asyncTx: " + err.Error())
}
jobID, status, usrErr, sysErr := api.InsertAsyncStatus(asyncTx, "DNSSEC refresh started")
if usrErr != nil || sysErr != nil {
unsetInDNSSECKeyRefresh()
return 0, false, status, usrErr, sysErr
}
go doDNSSECKeyRefresh(tx, db, tv, jobID, user) // doDNSSECKeyRefresh takes ownership of tx and MUST close it.
return jobID, true, http.StatusAccepted, nil, nil
} else {
log.Infoln("RefreshDNSSECKeys called, while server was concurrently executing a refresh, doing nothing")
return 0, false, http.StatusAccepted, nil, nil
}
}
const DNSSECKeyRefreshDefaultTTL = time.Duration(60) * time.Second
const DNSSECKeyRefreshDefaultGenerationMultiplier = uint64(10)
const DNSSECKeyRefreshDefaultEffectiveMultiplier = uint64(10)
const DNSSECKeyRefreshDefaultKSKExpiration = time.Duration(365) * time.Hour * 24
const DNSSECKeyRefreshDefaultZSKExpiration = time.Duration(30) * time.Hour * 24
// doDNSSECKeyRefresh refreshes the CDN's DNSSEC keys, as necessary.
// This takes ownership of tx, and MUST call `tx.Close()`.
// This SHOULD only be called if setInDNSSECKeyRefresh() returned true, in which case this MUST call unsetInDNSSECKeyRefresh() before returning.
func doDNSSECKeyRefresh(tx *sql.Tx, asyncDB *sqlx.DB, tv trafficvault.TrafficVault, jobID int, user *auth.CurrentUser) {
doCommit := true
defer func() {
if doCommit {
tx.Commit()
} else {
tx.Rollback()
}
}()
defer unsetInDNSSECKeyRefresh()
cdnDNSSECKeyParams, err := getDNSSECKeyRefreshParams(tx)
if err != nil {
log.Errorln("refreshing DNSSEC Keys: getting cdn parameters: " + err.Error())
doCommit = false
if asyncErr := api.UpdateAsyncStatus(asyncDB, api.AsyncFailed, "DNSSEC refresh failed", jobID, true); asyncErr != nil {
log.Errorf("updating async status for id %d: %v", jobID, asyncErr)
}
return
}
cdns := []string{}
for _, inf := range cdnDNSSECKeyParams {
if inf.DNSSECEnabled {
cdns = append(cdns, string(inf.CDNName))
}
}
// TODO change to return a slice, map is slow and unnecessary
dsInfo, err := getDNSSECKeyRefreshDSInfo(tx, cdns)
if err != nil {
log.Errorln("refreshing DNSSEC Keys: getting ds info: " + err.Error())
doCommit = false
if asyncErr := api.UpdateAsyncStatus(asyncDB, api.AsyncFailed, "DNSSEC refresh failed", jobID, true); asyncErr != nil {
log.Errorf("updating async status for id %d: %v", jobID, asyncErr)
}
return
}
dses := []string{}
for ds, _ := range dsInfo {
dses = append(dses, string(ds))
}
dsMatchlists, err := deliveryservice.GetDeliveryServicesMatchLists(dses, tx)
if err != nil {
log.Errorln("refreshing DNSSEC Keys: getting ds matchlists: " + err.Error())
doCommit = false
if asyncErr := api.UpdateAsyncStatus(asyncDB, api.AsyncFailed, "DNSSEC refresh failed", jobID, true); asyncErr != nil {
log.Errorf("updating async status for id %d: %v", jobID, asyncErr)
}
return
}
exampleURLs := map[tc.DeliveryServiceName][]string{}
for ds, inf := range dsInfo {
exampleURLs[ds] = deliveryservice.MakeExampleURLs(inf.Protocol, inf.Type, inf.RoutingName, dsMatchlists[string(ds)], inf.CDNDomain)
}
errCount := 0
updateCount := 0
putErr := false
for _, cdnInf := range cdnDNSSECKeyParams {
keys, ok, err := tv.GetDNSSECKeys(string(cdnInf.CDNName), tx, context.Background()) // TODO get all in a map beforehand
if err != nil {
log.Warnln("refreshing DNSSEC Keys: getting cdn '" + string(cdnInf.CDNName) + "' keys from Traffic Vault, skipping: " + err.Error())
continue
}
if !ok {
log.Warnln("refreshing DNSSEC Keys: cdn '" + string(cdnInf.CDNName) + "' has no keys in Traffic Vault, skipping")
continue
}
ttl := DNSSECKeyRefreshDefaultTTL
if cdnInf.TLDTTLsDNSKEY != nil {
ttl = time.Duration(*cdnInf.TLDTTLsDNSKEY) * time.Second
}
genMultiplier := DNSSECKeyRefreshDefaultGenerationMultiplier
if cdnInf.DNSKEYGenerationMultiplier != nil {
genMultiplier = *cdnInf.DNSKEYGenerationMultiplier
}
effectiveMultiplier := DNSSECKeyRefreshDefaultEffectiveMultiplier
if cdnInf.DNSKEYEffectiveMultiplier != nil {
effectiveMultiplier = *cdnInf.DNSKEYEffectiveMultiplier
}
nowPlusTTL := time.Now().Add(ttl * time.Duration(genMultiplier)) // "key_expiration" in the Perl this was transliterated from
defaultKSKExpiration := DNSSECKeyRefreshDefaultKSKExpiration
for _, key := range keys[string(cdnInf.CDNName)].KSK {
if key.Status != tc.DNSSECKeyStatusNew {
continue
}
defaultKSKExpiration = time.Unix(key.ExpirationDateUnix, 0).Sub(time.Unix(key.InceptionDateUnix, 0))
break
}
defaultZSKExpiration := DNSSECKeyRefreshDefaultZSKExpiration
for _, key := range keys[string(cdnInf.CDNName)].ZSK {
if key.Status != tc.DNSSECKeyStatusNew {
continue
}
expiration := time.Unix(key.ExpirationDateUnix, 0)
inception := time.Unix(key.InceptionDateUnix, 0)
defaultZSKExpiration = expiration.Sub(inception)
if expiration.After(nowPlusTTL) {
continue
}
log.Infoln("The ZSK keys for '" + string(cdnInf.CDNName) + "' are expired! Regenerating them now.")
effectiveDate := expiration.Add(ttl * time.Duration(effectiveMultiplier) * -1) // -1 to subtract
isKSK := false
cdnDNSDomain := cdnInf.CDNDomain + "."
newKeys, err := regenExpiredKeys(isKSK, cdnDNSDomain, keys[string(cdnInf.CDNName)], effectiveDate, false, false)
if err != nil {
log.Errorln("refreshing DNSSEC Keys: regenerating expired ZSK keys: " + err.Error())
errCount++
} else {
keys[string(cdnInf.CDNName)] = newKeys
updateCount++
}
}
for _, ds := range dsInfo {
if ds.CDNName != cdnInf.CDNName {
continue
}
if t := ds.Type; !t.UsesDNSSECKeys() {
continue
}
dsKeys, dsKeysExist := keys[string(ds.DSName)]
if !dsKeysExist {
log.Infoln("Keys do not exist for ds '" + string(ds.DSName) + "'")
cdnKeys, ok := keys[string(ds.CDNName)]
if !ok {
log.Errorln("refreshing DNSSEC Keys: cdn has no keys, cannot create ds keys")
errCount++
continue
}
overrideTTL := false
dsKeys, err := deliveryservice.CreateDNSSECKeys(exampleURLs[ds.DSName], cdnKeys, defaultKSKExpiration, defaultZSKExpiration, ttl, overrideTTL)
if err != nil {
log.Errorln("refreshing DNSSEC Keys: creating missing ds keys: " + err.Error())
errCount++
}
keys[string(ds.DSName)] = dsKeys
updateCount++
continue
}
for _, key := range dsKeys.KSK {
if key.Status != tc.DNSSECKeyStatusNew {
continue
}
expiration := time.Unix(key.ExpirationDateUnix, 0)
if expiration.After(nowPlusTTL) {
continue
}
log.Infoln("The KSK keys for '" + ds.DSName + "' are expired! Regenerating them now.")
effectiveDate := expiration.Add(ttl * time.Duration(effectiveMultiplier) * -1) // -1 to subtract
isKSK := true
newKeys, err := regenExpiredKeys(isKSK, string(ds.DSName), dsKeys, effectiveDate, false, false)
if err != nil {
log.Errorln("refreshing DNSSEC Keys: regenerating expired KSK keys for ds '" + string(ds.DSName) + "': " + err.Error())
errCount++
} else {
keys[string(ds.DSName)] = newKeys
updateCount++
}
}
for _, key := range dsKeys.ZSK {
if key.Status != tc.DNSSECKeyStatusNew {
continue
}
expiration := time.Unix(key.ExpirationDateUnix, 0)
if expiration.After(nowPlusTTL) {
continue
}
log.Infoln("The ZSK keys for '" + ds.DSName + "' are expired! Regenerating them now.")
effectiveDate := expiration.Add(ttl * time.Duration(effectiveMultiplier) * -1) // -1 to subtract
isKSK := false
newKeys, err := regenExpiredKeys(isKSK, string(ds.DSName), dsKeys, effectiveDate, false, false)
if err != nil {
log.Errorln("refreshing DNSSEC Keys: regenerating expired ZSK keys for ds '" + string(ds.DSName) + "': " + err.Error())
errCount++
} else {
if existingNewKeys, ok := keys[string(ds.DSName)]; ok {
existingNewKeys.ZSK = newKeys.ZSK
newKeys = existingNewKeys
}
keys[string(ds.DSName)] = newKeys
updateCount++
}
}
}
if updateCount > 0 {
if err := tv.PutDNSSECKeys(string(cdnInf.CDNName), keys, tx, context.Background()); err != nil {
log.Errorln("refreshing DNSSEC Keys: putting keys into Traffic Vault for cdn '" + string(cdnInf.CDNName) + "': " + err.Error())
putErr = true
}
}
}
clMsg := fmt.Sprintf("Refreshed %d DNSSEC keys", updateCount)
status := api.AsyncSucceeded
msg := fmt.Sprintf("DNSSEC refresh completed successfully (%d keys were updated)", updateCount)
if putErr {
status = api.AsyncFailed
msg = fmt.Sprintf("DNSSEC refresh failed (attempted to update %d keys, but an error occurred while attempting to store in Traffic Vault)", updateCount)
clMsg = fmt.Sprintf("Attempted to refresh %d DNSSEC keys, but an error occurred while attempting to store in Traffic Vault", updateCount)
} else if errCount > 0 {
status = api.AsyncFailed
msg = fmt.Sprintf("DNSSEC refresh failed (updated %d keys, but %d errors occurred)", updateCount, errCount)
clMsg = fmt.Sprintf("Refreshed %d DNSSEC keys, but %d errors occurred", updateCount, errCount)
}
if updateCount > 0 || errCount > 0 || putErr {
api.CreateChangeLogRawTx(api.ApiChange, clMsg, user, tx)
}
if asyncErr := api.UpdateAsyncStatus(asyncDB, status, msg, jobID, true); asyncErr != nil {
log.Errorf("updating async status for id %d: %v", jobID, asyncErr)
}
log.Infoln("Done refreshing DNSSEC keys")
}
type DNSSECKeyRefreshCDNInfo struct {
CDNName tc.CDNName
CDNDomain string
DNSSECEnabled bool
TLDTTLsDNSKEY *uint64
DNSKEYEffectiveMultiplier *uint64
DNSKEYGenerationMultiplier *uint64
}
// getDNSSECKeyRefreshParams returns returns the CDN's profile's tld.ttls.DNSKEY, DNSKEY.effective.multiplier, and DNSKEY.generation.multiplier parameters. If either parameter doesn't exist, nil is returned.
// If a CDN exists, but has no parameters, it is returned as a key in the map with a nil value.
func getDNSSECKeyRefreshParams(tx *sql.Tx) (map[tc.CDNName]DNSSECKeyRefreshCDNInfo, error) {
qry := `
WITH cdn_profile_ids AS (
SELECT
DISTINCT(c.name) as cdn_name,
c.domain_name as cdn_domain,
c.dnssec_enabled as cdn_dnssec_enabled,
MAX(p.id) as profile_id -- We only want 1 profile, so get the probably-newest if there's more than one.
FROM
cdn c
LEFT JOIN profile p ON c.id = p.cdn AND (p.type = '` + tc.TrafficRouterProfileType + `')
GROUP BY c.name, c.dnssec_enabled, c.domain_name
)
SELECT
DISTINCT(pi.cdn_name),
pi.cdn_domain,
pi.cdn_dnssec_enabled,
MAX(pa.name) as parameter_name,
MAX(pa.value) as parameter_value
FROM
cdn_profile_ids pi
LEFT JOIN profile pr ON pi.profile_id = pr.id
LEFT JOIN profile_parameter pp ON pr.id = pp.profile
LEFT JOIN parameter pa ON pp.parameter = pa.id AND (
pa.name = 'tld.ttls.DNSKEY'
OR pa.name = 'DNSKEY.effective.multiplier'
OR pa.name = 'DNSKEY.generation.multiplier'
)
GROUP BY pi.cdn_name, pi.cdn_domain, pi.cdn_dnssec_enabled
`
rows, err := tx.Query(qry)
if err != nil {
return nil, errors.New("getting cdn dnssec key refresh parameters: " + err.Error())
}
defer rows.Close()
params := map[tc.CDNName]DNSSECKeyRefreshCDNInfo{}
for rows.Next() {
cdnName := tc.CDNName("")
cdnDomain := ""
dnssecEnabled := false
name := util.StrPtr("")
valStr := util.StrPtr("")
if err := rows.Scan(&cdnName, &cdnDomain, &dnssecEnabled, &name, &valStr); err != nil {
return nil, errors.New("scanning cdn dnssec key refresh parameters: " + err.Error())
}
inf := params[cdnName]
inf.CDNName = cdnName
inf.CDNDomain = cdnDomain
inf.DNSSECEnabled = dnssecEnabled
if name == nil || valStr == nil {
// no DNSKEY parameters, but the CDN still exists.
params[cdnName] = inf
continue
}
val, err := strconv.ParseUint(*valStr, 10, 64)
if err != nil {
log.Warnln("getting CDN dnssec refresh parameters: parameter '" + *name + "' value '" + *valStr + "' is not a number, skipping")
params[cdnName] = inf
continue
}
switch *name {
case "tld.ttls.DNSKEY":
inf.TLDTTLsDNSKEY = &val
case "DNSKEY.effective.multiplier":
inf.DNSKEYEffectiveMultiplier = &val
case "DNSKEY.generation.multiplier":
inf.DNSKEYGenerationMultiplier = &val
default:
log.Warnln("getDNSSECKeyRefreshParams got unknown parameter '" + *name + "', skipping")
continue
}
params[cdnName] = inf
}
return params, nil
}
type DNSSECKeyRefreshDSInfo struct {
DSName tc.DeliveryServiceName
Type tc.DSType
Protocol *int
CDNName tc.CDNName
CDNDomain string
RoutingName string
}
func getDNSSECKeyRefreshDSInfo(tx *sql.Tx, cdns []string) (map[tc.DeliveryServiceName]DNSSECKeyRefreshDSInfo, error) {
qry := `
SELECT
ds.xml_id,
tp.name as type,
ds.protocol,
c.name as cdn_name,
c.domain_name as cdn_domain,
ds.routing_name
FROM
deliveryservice ds
JOIN type tp ON tp.id = ds.type
JOIN cdn c ON c.id = ds.cdn_id
WHERE
c.name = ANY($1)
`
rows, err := tx.Query(qry, pq.Array(cdns))
if err != nil {
return nil, errors.New("getting cdn dnssec key refresh ds info: " + err.Error())
}
defer rows.Close()
dsInf := map[tc.DeliveryServiceName]DNSSECKeyRefreshDSInfo{}
for rows.Next() {
i := DNSSECKeyRefreshDSInfo{}
if err := rows.Scan(&i.DSName, &i.Type, &i.Protocol, &i.CDNName, &i.CDNDomain, &i.RoutingName); err != nil {
return nil, errors.New("scanning cdn dnssec key refresh ds info: " + err.Error())
}
dsInf[i.DSName] = i
}
return dsInf, nil
}
// inDNSSECKeyRefresh is whether the server is currently processing a refresh in the background.
// This is used to only perform 1 refresh at a time.
// This MUST NOT be changed outside of atomic operations.
// This MUST NOT be changed to a boolean, or set without atomics. Atomic semantics involve more than just setting a memory location.
var inDNSSECKeyRefresh = uint64(0)
// setInDNSSECKeyRefresh attempts to set whether the server is currently executing a DNSSEC key refresh operation.
// Returns false if a refresh operation is already executing.
// If this returns true, the caller MUST call unsetInDNSSECKeyRefresh().
func setInDNSSECKeyRefresh() bool { return atomic.CompareAndSwapUint64(&inDNSSECKeyRefresh, 0, 1) }
// unsetInDNSSECKeyRefresh sets the flag indicating that the server is currently executing a DNSSEC key refresh operation to false.
// This MUST NOT be called, unless setInDNSSECKeyRefresh() was previously called and returned true.
func unsetInDNSSECKeyRefresh() { atomic.StoreUint64(&inDNSSECKeyRefresh, 0) }