mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-07 01:56:57 +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
|
## 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.
|
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
|
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, using the `--txt-encrypt-aes-key` flag.
|
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.
|
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() {
|
func main() {
|
||||||
key := []byte("testtesttesttesttesttesttesttest")
|
keys := []string{
|
||||||
encrypted, _ := endpoint.EncryptText(
|
"ZPitL0NGVQBZbTD6DwXJzD8RiStSazzYXQsdUowLURY=", // safe base64 url encoded 44 bytes and 32 when decoded
|
||||||
"heritage=external-dns,external-dns/owner=example,external-dns/resource=ingress/default/example",
|
"01234567890123456789012345678901", // plain txt 32 bytes
|
||||||
key,
|
"passphrasewhichneedstobe32bytes!", // plain txt 32 bytes
|
||||||
nil,
|
}
|
||||||
)
|
|
||||||
decrypted, _, _ := endpoint.DecryptText(encrypted, key)
|
for _, k := range keys {
|
||||||
fmt.Println(decrypted)
|
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
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"crypto/rand"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"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.")
|
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) {
|
func NewLabelsFromString(labelText string, aesKey []byte) (Labels, error) {
|
||||||
if len(aesKey) != 0 {
|
if len(aesKey) != 0 {
|
||||||
decryptedText, encryptionNonce, err := DecryptText(strings.Trim(labelText, "\""), aesKey)
|
decryptedText, encryptionNonce, err := DecryptText(strings.Trim(labelText, "\""), aesKey)
|
||||||
//in case if we have decryption error, just try process original text
|
// 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
|
// decryption errors should be ignored here, because we can already have plain-text labels in registry
|
||||||
if err == nil {
|
if err == nil {
|
||||||
labels, err := NewLabelsFromStringPlain(decryptedText)
|
labels, err := NewLabelsFromStringPlain(decryptedText)
|
||||||
if err == nil {
|
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)
|
log.Debugf("Encrypt the serialized text %#v before returning it.", text)
|
||||||
var err error
|
var err error
|
||||||
text, err = EncryptText(text, aesKey, encryptionNonce)
|
text, err = EncryptText(text, aesKey, encryptionNonce)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
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
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -79,6 +82,60 @@ func (suite *LabelsSuite) TestEncryptionNonceReUsage() {
|
|||||||
suite.Equal(serialized, suite.fooAsTextEncrypted, "serialized result should be equal")
|
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() {
|
func (suite *LabelsSuite) TestDeserialize() {
|
||||||
foo, err := NewLabelsFromStringPlain(suite.fooAsText)
|
foo, err := NewLabelsFromStringPlain(suite.fooAsText)
|
||||||
suite.NoError(err, "should succeed for valid label text")
|
suite.NoError(err, "should succeed for valid label text")
|
||||||
|
@ -18,6 +18,7 @@ package registry
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
b64 "encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
@ -84,7 +85,10 @@ func NewDynamoDBRegistry(provider provider.Provider, ownerID string, dynamodbAPI
|
|||||||
if len(txtEncryptAESKey) == 0 {
|
if len(txtEncryptAESKey) == 0 {
|
||||||
txtEncryptAESKey = nil
|
txtEncryptAESKey = nil
|
||||||
} else if len(txtEncryptAESKey) != 32 {
|
} 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 {
|
if len(txtPrefix) > 0 && len(txtSuffix) > 0 {
|
||||||
return nil, errors.New("txt-prefix and txt-suffix are mutually exclusive")
|
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")
|
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)
|
_, 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)
|
_, 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")
|
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) {
|
func TestDynamoDBRegistryRecordsBadTable(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -22,6 +22,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
b64 "encoding/base64"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"sigs.k8s.io/external-dns/endpoint"
|
"sigs.k8s.io/external-dns/endpoint"
|
||||||
@ -63,11 +65,16 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st
|
|||||||
if ownerID == "" {
|
if ownerID == "" {
|
||||||
return nil, errors.New("owner id cannot be empty")
|
return nil, errors.New("owner id cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(txtEncryptAESKey) == 0 {
|
if len(txtEncryptAESKey) == 0 {
|
||||||
txtEncryptAESKey = nil
|
txtEncryptAESKey = nil
|
||||||
} else if len(txtEncryptAESKey) != 32 {
|
} 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 {
|
if txtEncryptEnabled && txtEncryptAESKey == nil {
|
||||||
return nil, errors.New("the AES Encryption key must be set when TXT record encryption is enabled")
|
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.
|
// We simply assume that TXT records for the registry will always have only one target.
|
||||||
labels, err := endpoint.NewLabelsFromString(record.Targets[0], im.txtEncryptAESKey)
|
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
|
// if no heritage is found or it is invalid
|
||||||
// case when value of txt record cannot be identified
|
// case when value of txt record cannot be identified
|
||||||
// record will not be removed as it will have empty owner
|
// 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
|
txtNew.ProviderSpecific = r.ProviderSpecific
|
||||||
endpoints = append(endpoints, txtNew)
|
endpoints = append(endpoints, txtNew)
|
||||||
}
|
}
|
||||||
|
|
||||||
return endpoints
|
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