mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 01:26:59 +02:00
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:
commit
290f8c848d
@ -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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
298
registry/txt_encryption_test.go
Normal file
298
registry/txt_encryption_test.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user