Merge pull request #4980 from gofogo/fix-aes-4976

fix(aes-encryption): support plain txt and url safe base64 strings
This commit is contained in:
Kubernetes Prow Robot 2025-01-23 01:06:57 -08:00 committed by GitHub
commit 290f8c848d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 475 additions and 19 deletions

View File

@ -26,11 +26,11 @@ wildcard domains will have invalid domain syntax and be rejected by most provide
## Encryption
Registry TXT records may contain information, such as the internal ingress name or namespace, considered sensitive, , which attackers could exploit to gather information about your infrastructure.
Registry TXT records may contain information, such as the internal ingress name or namespace, considered sensitive, , which attackers could exploit to gather information about your infrastructure.
By encrypting TXT records, you can protect this information from unauthorized access.
Encryption is enabled by using the `--txt-encrypt-enabled` flag. The 32-byte AES-256-GCM encryption
key must be specified in URL-safe base64 form, using the `--txt-encrypt-aes-key` flag.
Encryption is enabled by setting the `--txt-encrypt-enabled`. The 32-byte AES-256-GCM encryption
key must be specified in URL-safe base64 form (recommended) or be a plain text, using the `--txt-encrypt-aes-key=<key>` flag.
Note that the key used for encryption should be a secure key and properly managed to ensure the security of your TXT records.
@ -78,14 +78,32 @@ import (
)
func main() {
key := []byte("testtesttesttesttesttesttesttest")
encrypted, _ := endpoint.EncryptText(
"heritage=external-dns,external-dns/owner=example,external-dns/resource=ingress/default/example",
key,
nil,
)
decrypted, _, _ := endpoint.DecryptText(encrypted, key)
fmt.Println(decrypted)
keys := []string{
"ZPitL0NGVQBZbTD6DwXJzD8RiStSazzYXQsdUowLURY=", // safe base64 url encoded 44 bytes and 32 when decoded
"01234567890123456789012345678901", // plain txt 32 bytes
"passphrasewhichneedstobe32bytes!", // plain txt 32 bytes
}
for _, k := range keys {
key := []byte(k)
if len(key) != 32 {
// if key is not a plain txt let's decode
var err error
if key, err = b64.StdEncoding.DecodeString(string(key)); err != nil || len(key) != 32 {
fmt.Errorf("the AES Encryption key must have a length of 32 byte")
}
}
encrypted, _ := endpoint.EncryptText(
"heritage=external-dns,external-dns/owner=example,external-dns/resource=ingress/default/example",
key,
nil,
)
decrypted, _, err := endpoint.DecryptText(encrypted, key)
if err != nil {
fmt.Println("Error decrypting:", err, "for key:", k)
}
fmt.Println(decrypted)
}
}
```

View File

@ -17,8 +17,12 @@ limitations under the License.
package endpoint
import (
"encoding/base64"
"io"
"testing"
"crypto/rand"
"github.com/stretchr/testify/require"
)
@ -56,3 +60,33 @@ func TestEncrypt(t *testing.T) {
t.Error("Decryption of text didn't result in expected plaintext result.")
}
}
func TestGenerateNonceSuccess(t *testing.T) {
nonce, err := GenerateNonce()
require.NoError(t, err)
require.NotEmpty(t, nonce)
// Test nonce length
decodedNonce, err := base64.StdEncoding.DecodeString(string(nonce))
require.NoError(t, err)
require.Equal(t, standardGcmNonceSize, len(decodedNonce))
}
func TestGenerateNonceError(t *testing.T) {
// Save the original rand.Reader
originalRandReader := rand.Reader
defer func() { rand.Reader = originalRandReader }()
// Replace rand.Reader with a faulty reader
rand.Reader = &faultyReader{}
nonce, err := GenerateNonce()
require.Error(t, err)
require.Nil(t, nonce)
}
type faultyReader struct{}
func (f *faultyReader) Read(p []byte) (n int, err error) {
return 0, io.ErrUnexpectedEOF
}

View File

@ -93,8 +93,8 @@ func NewLabelsFromStringPlain(labelText string) (Labels, error) {
func NewLabelsFromString(labelText string, aesKey []byte) (Labels, error) {
if len(aesKey) != 0 {
decryptedText, encryptionNonce, err := DecryptText(strings.Trim(labelText, "\""), aesKey)
//in case if we have decryption error, just try process original text
//decryption errors should be ignored here, because we can already have plain-text labels in registry
// in case if we have decryption error, just try process original text
// decryption errors should be ignored here, because we can already have plain-text labels in registry
if err == nil {
labels, err := NewLabelsFromStringPlain(decryptedText)
if err == nil {
@ -152,8 +152,8 @@ func (l Labels) Serialize(withQuotes bool, txtEncryptEnabled bool, aesKey []byte
log.Debugf("Encrypt the serialized text %#v before returning it.", text)
var err error
text, err = EncryptText(text, aesKey, encryptionNonce)
if err != nil {
// if encryption failed, the external-dns will crash
log.Fatalf("Failed to encrypt the text %#v using the encryption key %#v. Got error %#v.", text, aesKey, err)
}

View File

@ -17,9 +17,12 @@ limitations under the License.
package endpoint
import (
"bytes"
"crypto/rand"
"fmt"
"testing"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/suite"
)
@ -79,6 +82,60 @@ func (suite *LabelsSuite) TestEncryptionNonceReUsage() {
suite.Equal(serialized, suite.fooAsTextEncrypted, "serialized result should be equal")
}
func (suite *LabelsSuite) TestEncryptionKeyChanged() {
foo, err := NewLabelsFromString(suite.fooAsTextEncrypted, suite.aesKey)
suite.NoError(err, "should succeed for valid label text")
serialised := foo.Serialize(false, true, []byte("passphrasewhichneedstobe32bytes!"))
suite.NotEqual(serialised, suite.fooAsTextEncrypted, "serialized result should be equal")
}
func (suite *LabelsSuite) TestEncryptionFailed() {
foo, err := NewLabelsFromString(suite.fooAsTextEncrypted, suite.aesKey)
suite.NoError(err, "should succeed for valid label text")
defer func() { log.StandardLogger().ExitFunc = nil }()
b := new(bytes.Buffer)
var fatalCrash bool
log.StandardLogger().ExitFunc = func(int) { fatalCrash = true }
log.StandardLogger().SetOutput(b)
_ = foo.Serialize(false, true, []byte("wrong-key"))
suite.True(fatalCrash, "should fail if encryption key is wrong")
suite.Contains(b.String(), "Failed to encrypt the text")
}
func (suite *LabelsSuite) TestEncryptionFailedFaultyReader() {
foo, err := NewLabelsFromString(suite.fooAsTextEncrypted, suite.aesKey)
suite.NoError(err, "should succeed for valid label text")
// remove encryption nonce just for simplicity, so that we could regenerate nonce
delete(foo, txtEncryptionNonce)
originalRandReader := rand.Reader
defer func() {
log.StandardLogger().ExitFunc = nil
rand.Reader = originalRandReader
}()
// Replace rand.Reader with a faulty reader
rand.Reader = &faultyReader{}
b := new(bytes.Buffer)
var fatalCrash bool
log.StandardLogger().ExitFunc = func(int) { fatalCrash = true }
log.StandardLogger().SetOutput(b)
_ = foo.Serialize(false, true, suite.aesKey)
suite.True(fatalCrash)
suite.Contains(b.String(), "Failed to generate cryptographic nonce")
}
func (suite *LabelsSuite) TestDeserialize() {
foo, err := NewLabelsFromStringPlain(suite.fooAsText)
suite.NoError(err, "should succeed for valid label text")

View File

@ -18,6 +18,7 @@ package registry
import (
"context"
b64 "encoding/base64"
"errors"
"fmt"
"strings"
@ -84,7 +85,10 @@ func NewDynamoDBRegistry(provider provider.Provider, ownerID string, dynamodbAPI
if len(txtEncryptAESKey) == 0 {
txtEncryptAESKey = nil
} else if len(txtEncryptAESKey) != 32 {
return nil, errors.New("the AES Encryption key must have a length of 32 bytes")
var err error
if txtEncryptAESKey, err = b64.StdEncoding.DecodeString(string(txtEncryptAESKey)); err != nil || len(txtEncryptAESKey) != 32 {
return nil, errors.New("the AES Encryption key must be 32 bytes long, in either plain text or base64-encoded format")
}
}
if len(txtPrefix) > 0 && len(txtSuffix) > 0 {
return nil, errors.New("txt-prefix and txt-suffix are mutually exclusive")

View File

@ -61,12 +61,51 @@ func TestDynamoDBRegistryNew(t *testing.T) {
require.EqualError(t, err, "table cannot be empty")
_, err = NewDynamoDBRegistry(p, "test-owner", api, "test-table", "", "", "", []string{}, []string{}, []byte(";k&l)nUC/33:{?d{3)54+,AD?]SX%yh^x"), time.Hour)
require.EqualError(t, err, "the AES Encryption key must have a length of 32 bytes")
require.EqualError(t, err, "the AES Encryption key must be 32 bytes long, in either plain text or base64-encoded format")
_, err = NewDynamoDBRegistry(p, "test-owner", api, "test-table", "testPrefix", "testSuffix", "", []string{}, []string{}, []byte(""), time.Hour)
require.EqualError(t, err, "txt-prefix and txt-suffix are mutually exclusive")
}
func TestDynamoDBRegistryNew_EncryptionConfig(t *testing.T) {
api, p := newDynamoDBAPIStub(t, nil)
tests := []struct {
encEnabled bool
aesKeyRaw []byte
aesKeySanitized []byte
errorExpected bool
}{
{
encEnabled: true,
aesKeyRaw: []byte("123456789012345678901234567890asdfasdfasdfasdfa12"),
aesKeySanitized: []byte{},
errorExpected: true,
},
{
encEnabled: true,
aesKeyRaw: []byte("passphrasewhichneedstobe32bytes!"),
aesKeySanitized: []byte("passphrasewhichneedstobe32bytes!"),
errorExpected: false,
},
{
encEnabled: true,
aesKeyRaw: []byte("ZPitL0NGVQBZbTD6DwXJzD8RiStSazzYXQsdUowLURY="),
aesKeySanitized: []byte{100, 248, 173, 47, 67, 70, 85, 0, 89, 109, 48, 250, 15, 5, 201, 204, 63, 17, 137, 43, 82, 107, 60, 216, 93, 11, 29, 82, 140, 11, 81, 22},
errorExpected: false,
},
}
for _, test := range tests {
actual, err := NewDynamoDBRegistry(p, "test-owner", api, "test-table", "", "", "", []string{}, []string{}, test.aesKeyRaw, time.Hour)
if test.errorExpected {
require.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.aesKeySanitized, actual.txtEncryptAESKey)
}
}
}
func TestDynamoDBRegistryRecordsBadTable(t *testing.T) {
for _, tc := range []struct {
name string

View File

@ -22,6 +22,8 @@ import (
"strings"
"time"
b64 "encoding/base64"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint"
@ -63,11 +65,16 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st
if ownerID == "" {
return nil, errors.New("owner id cannot be empty")
}
if len(txtEncryptAESKey) == 0 {
txtEncryptAESKey = nil
} else if len(txtEncryptAESKey) != 32 {
return nil, errors.New("the AES Encryption key must have a length of 32 bytes")
var err error
if txtEncryptAESKey, err = b64.StdEncoding.DecodeString(string(txtEncryptAESKey)); err != nil || len(txtEncryptAESKey) != 32 {
return nil, errors.New("the AES Encryption key must be 32 bytes long, in either plain text or base64-encoded format")
}
}
if txtEncryptEnabled && txtEncryptAESKey == nil {
return nil, errors.New("the AES Encryption key must be set when TXT record encryption is enabled")
}
@ -131,7 +138,7 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error
}
// We simply assume that TXT records for the registry will always have only one target.
labels, err := endpoint.NewLabelsFromString(record.Targets[0], im.txtEncryptAESKey)
if err == endpoint.ErrInvalidHeritage {
if errors.Is(err, endpoint.ErrInvalidHeritage) {
// if no heritage is found or it is invalid
// case when value of txt record cannot be identified
// record will not be removed as it will have empty owner
@ -237,7 +244,6 @@ func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpo
txtNew.ProviderSpecific = r.ProviderSpecific
endpoints = append(endpoints, txtNew)
}
return endpoints
}

View File

@ -0,0 +1,298 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed 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.
*/
package registry
import (
"context"
"fmt"
"slices"
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider/inmemory"
)
func TestNewTXTRegistryEncryptionConfig(t *testing.T) {
p := inmemory.NewInMemoryProvider()
tests := []struct {
encEnabled bool
aesKeyRaw []byte
aesKeySanitized []byte
errorExpected bool
}{
{
encEnabled: true,
aesKeyRaw: []byte("123456789012345678901234567890asdfasdfasdfasdfa12"),
aesKeySanitized: []byte{},
errorExpected: true,
},
{
encEnabled: true,
aesKeyRaw: []byte("passphrasewhichneedstobe32bytes!"),
aesKeySanitized: []byte("passphrasewhichneedstobe32bytes!"),
errorExpected: false,
},
{
encEnabled: true,
aesKeyRaw: []byte("ZPitL0NGVQBZbTD6DwXJzD8RiStSazzYXQsdUowLURY="),
aesKeySanitized: []byte{100, 248, 173, 47, 67, 70, 85, 0, 89, 109, 48, 250, 15, 5, 201, 204, 63, 17, 137, 43, 82, 107, 60, 216, 93, 11, 29, 82, 140, 11, 81, 22},
errorExpected: false,
},
}
for _, test := range tests {
actual, err := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "", []string{}, []string{}, test.encEnabled, test.aesKeyRaw)
if test.errorExpected {
require.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.aesKeySanitized, actual.txtEncryptAESKey)
}
}
}
func TestGenerateTXTGenerateTextRecordEncryptionWihDecryption(t *testing.T) {
p := inmemory.NewInMemoryProvider()
_ = p.CreateZone(testZone)
tests := []struct {
record *endpoint.Endpoint
decrypted string
}{
{
record: newEndpointWithOwner("foo.test-zone.example.org", "new-foo.loadbalancer.com", endpoint.RecordTypeCNAME, "owner-2"),
decrypted: "heritage=external-dns,external-dns/owner=owner-2",
},
{
record: newEndpointWithOwnerAndLabels("foo.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "owner-1", endpoint.Labels{endpoint.OwnedRecordLabelKey: "foo.test-zone.example.org"}),
decrypted: "heritage=external-dns,external-dns/ownedRecord=foo.test-zone.example.org,external-dns/owner=owner-1",
},
{
record: newEndpointWithOwnerAndLabels("bar.test-zone.example.org", "cluster-b", endpoint.RecordTypeCNAME, "owner-1", endpoint.Labels{endpoint.ResourceLabelKey: "ingress/default/foo-127"}),
decrypted: "heritage=external-dns,external-dns/owner=owner-1,external-dns/resource=ingress/default/foo-127",
},
{
record: newEndpointWithOwner("dualstack.test-zone.example.org", "1.1.1.1", endpoint.RecordTypeA, "owner-0"),
decrypted: "heritage=external-dns,external-dns/owner=owner-0",
},
}
withEncryptionKeys := []string{
"passphrasewhichneedstobe32bytes!",
"ZPitL0NGVQBZbTD6DwXJzD8RiStSazzYXQsdUowLURY=",
"01234567890123456789012345678901",
}
for _, test := range tests {
for _, k := range withEncryptionKeys {
t.Run(fmt.Sprintf("key '%s' with decrypted result '%s'", k, test.decrypted), func(t *testing.T) {
key := []byte(k)
r, err := NewTXTRegistry(p, "", "", "owner", time.Minute, "", []string{}, []string{}, true, key)
assert.NoError(t, err, "Error creating TXT registry")
txtRecords := r.generateTXTRecord(test.record)
assert.Len(t, txtRecords, len(test.record.Targets))
for _, txt := range txtRecords {
// should return a TXT record with the encryption nonce label. At the moment nonce is not set as label.
assert.NotContains(t, txt.Labels, "txt-encryption-nonce")
assert.Len(t, txt.Targets, 1)
assert.LessOrEqual(t, len(txt.Targets), 1)
// decrypt targets
for _, target := range txtRecords[0].Targets {
encryptedText, errUnquote := strconv.Unquote(target)
assert.NoError(t, errUnquote, "Error unquoting the encrypted text")
actual, nonce, errDecrypt := endpoint.DecryptText(encryptedText, r.txtEncryptAESKey)
assert.NoError(t, errDecrypt, "Error decrypting the encrypted text")
assert.True(t, strings.HasPrefix(encryptedText, nonce),
fmt.Sprintf("Nonce '%s' should be a prefix of the encrypted text: '%s'", nonce, encryptedText))
assert.Equal(t, test.decrypted, actual)
}
}
})
}
}
}
func TestApplyRecordsWithEncryption(t *testing.T) {
ctx := context.Background()
p := inmemory.NewInMemoryProvider()
_ = p.CreateZone("org")
key := []byte("ZPitL0NGVQBZbTD6DwXJzD8RiStSazzYXQsdUowLURY=")
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, key)
_ = r.ApplyChanges(ctx, &plan.Changes{
Create: []*endpoint.Endpoint{
newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwnerAndOwnedRecord("new-record-2.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "new-record-1.test-zone.example.org"),
newEndpointWithOwner("example.org", "new-loadbalancer-3.org", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwnerAndOwnedRecord("main.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "example"),
newEndpointWithOwner("tar.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner-2"),
newEndpointWithOwner("thing3.org", "1.2.3.4", endpoint.RecordTypeA, "owner"),
newEndpointWithOwner("thing4.org", "2001:DB8::2", endpoint.RecordTypeAAAA, "owner"),
},
})
allPlainTextTargetsToAssert := []string{
"heritage=external-dns,external-dns/",
"tar.loadbalancer.com",
"new-loadbalancer-1.lb.com",
"2001:DB8::2",
"new-loadbalancer-3.org",
"1.2.3.4",
}
records, _ := p.Records(ctx)
assert.Len(t, records, 14)
for _, r := range records {
if r.RecordType == endpoint.RecordTypeTXT && (strings.HasPrefix(r.DNSName, "cname-") || strings.HasPrefix(r.DNSName, "txt-new-")) {
assert.NotContains(t, r.Labels, "txt-encryption-nonce")
// assuming single target, it should be not a plain text
assert.NotContains(t, r.Targets[0], "heritage=external-dns")
}
// All TXT records with new- prefix should have the encryption nonce label and be in plain text
if r.RecordType == endpoint.RecordTypeTXT && strings.HasPrefix(r.DNSName, "new-") {
assert.Contains(t, r.Labels, "txt-encryption-nonce")
// assuming single target, it should be in a plain text
assert.Contains(t, r.Targets[0], "heritage=external-dns,external-dns/")
}
// All CNAME, A and AAAA TXT records should have the encryption nonce label
if slices.Contains([]string{"CNAME", "A", "AAAA"}, r.RecordType) {
assert.Contains(t, r.Labels, "txt-encryption-nonce")
// validate that target is in plain text
assert.Contains(t, allPlainTextTargetsToAssert, r.Targets[0])
}
}
}
func TestApplyRecordsWithEncryptionKeyChanged(t *testing.T) {
ctx := context.Background()
p := inmemory.NewInMemoryProvider()
_ = p.CreateZone("org")
withEncryptionKeys := []string{
"passphrasewhichneedstobe32bytes!",
"ZPitL0NGVQBZbTD6DwXJzD8RiStSazzYXQsdUowLURY=",
"01234567890123456789012345678901",
}
for _, key := range withEncryptionKeys {
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte(key))
_ = r.ApplyChanges(ctx, &plan.Changes{
Create: []*endpoint.Endpoint{
newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwnerAndOwnedRecord("new-record-2.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "new-record-1.test-zone.example.org"),
newEndpointWithOwner("example.org", "new-loadbalancer-3.org", endpoint.RecordTypeCNAME, "owner"),
newEndpointWithOwnerAndOwnedRecord("main.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "example"),
newEndpointWithOwner("tar.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner-2"),
newEndpointWithOwner("thing3.org", "1.2.3.4", endpoint.RecordTypeA, "owner"),
newEndpointWithOwner("thing4.org", "2001:DB8::2", endpoint.RecordTypeAAAA, "owner"),
},
})
}
records, _ := p.Records(ctx)
assert.Len(t, records, 14)
}
func TestApplyRecordsOnEncryptionKeyChangeWithKeyIdLabel(t *testing.T) {
ctx := context.Background()
p := inmemory.NewInMemoryProvider()
_ = p.CreateZone("org")
withEncryptionKeys := []string{
"passphrasewhichneedstobe32bytes!",
"ZPitL0NGVQBZbTD6DwXJzD8RiStSazzYXQsdUowLURY=",
"01234567890123456789012345678901",
}
for i, key := range withEncryptionKeys {
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "", []string{}, []string{}, true, []byte(key))
keyId := fmt.Sprintf("key-id-%d", i)
changes := []*endpoint.Endpoint{
newEndpointWithOwnerAndOwnedRecordWithKeyIDLabel("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner", "", keyId),
newEndpointWithOwnerAndOwnedRecordWithKeyIDLabel("new-record-2.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "new-record-1.test-zone.example.org", keyId),
newEndpointWithOwnerAndOwnedRecordWithKeyIDLabel("example.org", "new-loadbalancer-3.org", endpoint.RecordTypeCNAME, "owner", "", keyId),
newEndpointWithOwnerAndOwnedRecordWithKeyIDLabel("main.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, "", "example", keyId),
newEndpointWithOwnerAndOwnedRecordWithKeyIDLabel("tar.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner-2", "", keyId),
newEndpointWithOwnerAndOwnedRecordWithKeyIDLabel("thing3.org", "1.2.3.4", endpoint.RecordTypeA, "owner", "", keyId),
newEndpointWithOwnerAndOwnedRecordWithKeyIDLabel("thing4.org", "2001:DB8::2", endpoint.RecordTypeAAAA, "owner", "", keyId),
}
if i == 0 {
_ = r.ApplyChanges(ctx, &plan.Changes{
Create: changes,
})
} else {
_ = r.ApplyChanges(context.Background(), &plan.Changes{
UpdateNew: changes,
})
}
}
records, _ := p.Records(ctx)
assert.Len(t, records, 14)
encryptionNonce := map[string]bool{}
for _, r := range records {
if slices.Contains([]string{"A", "AAAA"}, r.RecordType) || (r.RecordType == "CNAME" && strings.HasPrefix(r.DNSName, "new-")) {
assert.Contains(t, r.Labels, "key-id")
assert.Equal(t, "key-id-2", r.Labels["key-id"])
// add encryption nonce to track the number of unique nonce
encryptionNonce[r.Labels["txt-encryption-nonce"]] = true
} else if r.RecordType == endpoint.RecordTypeTXT {
if hasPrefixFromSlice(r.DNSName, []string{"cname-", "txt-new-", "a-", "aaaa-", "txt-"}) {
assert.NotContains(t, r.Labels, "key-id")
} else {
assert.Contains(t, r.Labels, "key-id", r.DNSName)
assert.Equal(t, "key-id-0", r.Labels["key-id"], r.DNSName)
// add encryption nonce to track the number of unique nonce
encryptionNonce[r.Labels["txt-encryption-nonce"]] = true
}
}
}
assert.LessOrEqual(t, len(encryptionNonce), 5)
}
func hasPrefixFromSlice(str string, prefixes []string) bool {
for _, prefix := range prefixes {
if strings.HasPrefix(str, prefix) {
return true
}
}
return false
}
func newEndpointWithOwnerAndOwnedRecordWithKeyIDLabel(dnsName, target, recordType, ownerID string, resource string, keyId string) *endpoint.Endpoint {
e := endpoint.NewEndpoint(dnsName, recordType, target)
e.Labels[endpoint.OwnerLabelKey] = ownerID
e.Labels[endpoint.ResourceLabelKey] = resource
e.Labels["key-id"] = keyId
return e
}