Skip to content

Commit

Permalink
encrypt rotate-key command implementation.
Browse files Browse the repository at this point in the history
Signed-off-by: viktor-kurchenko <[email protected]>
  • Loading branch information
viktor-kurchenko committed Jan 27, 2024
1 parent 0997a92 commit 7748386
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 0 deletions.
109 changes: 109 additions & 0 deletions encrypt/ipsec_rotate_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package encrypt

import (
"context"
"crypto/rand"
"fmt"
"regexp"
"strconv"
"strings"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"

"github.com/cilium/cilium-cli/defaults"
)

type ipsecKey struct {
id int
algo string
random string
size int
}

// IPsecRotateKey rotates IPsec key.
func (s *Status) IPsecRotateKey(ctx context.Context) error {
ctx, cancelFn := context.WithTimeout(ctx, s.params.WaitDuration)
defer cancelFn()

secret, err := s.client.GetSecret(ctx, s.params.CiliumNamespace, defaults.EncryptionSecretName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to fetch IPsec secret: %s", err)
}

key, err := ipsecKeyFromString(string(secret.Data["keys"]))
if err != nil {
return err
}

newKey, err := key.rotate()
if err != nil {
return fmt.Errorf("failed to rotate IPsec key: %s", err)
}

patch := []byte(`{"stringData":{"keys":"` + newKey.String() + `"}}`)
_, err = s.client.PatchSecret(ctx, s.params.CiliumNamespace, defaults.EncryptionSecretName, types.StrategicMergePatchType, patch, metav1.PatchOptions{})
if err != nil {
return fmt.Errorf("failed to patch IPsec secret with new key: %s", err)
}

_, err = fmt.Printf("IPsec key successfully rotated, new key ID: %d\n", newKey.id)
return err
}

func (k ipsecKey) String() string {
return fmt.Sprintf("%d %s %s %d", k.id, k.algo, k.random, k.size)
}

var ipsecKeyRegex = regexp.MustCompile(`^([[:digit:]]+)[[:space:]](\S+)[[:space:]]([[:alnum:]]+)[[:space:]]([[:digit:]]+)$`)

func ipsecKeyFromString(s string) (ipsecKey, error) {
parts := ipsecKeyRegex.FindStringSubmatch(s)
if len(parts) != 5 {
return ipsecKey{}, fmt.Errorf("IPsec key has unsupported format")
}
id, err := strconv.Atoi(parts[1])
if err != nil {
return ipsecKey{}, fmt.Errorf("invalid IPsec key ID: %s", parts[1])
}
size, err := strconv.Atoi(parts[4])
if err != nil {
return ipsecKey{}, fmt.Errorf("invalid IPsec key size: %s", parts[4])
}
key := ipsecKey{
id: id,
algo: parts[2],
random: parts[3],
size: size,
}
return key, nil
}

const maxIPsecKeyID = 14

func (k ipsecKey) rotate() (ipsecKey, error) {
buf := make([]byte, len(k.random)/2)
if _, err := rand.Read(buf); err != nil {
return ipsecKey{}, fmt.Errorf("failed to generate random part: %s", err)
}
random := &strings.Builder{}
random.Grow(len(buf))
for _, c := range buf {
random.WriteString(fmt.Sprintf("%02x", c))
}

id := k.id + 1
if id > maxIPsecKeyID {
id = 0
}
key := ipsecKey{
id: id,
algo: k.algo,
random: random.String(),
size: k.size,
}
return key, nil
}
100 changes: 100 additions & 0 deletions encrypt/ipsec_rotate_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package encrypt

import (
"testing"

"github.com/stretchr/testify/require"
)

func Test_ipsecKeyFromString(t *testing.T) {
testCases := []struct {
have string
expected ipsecKey
}{
{
have: "3 rfc4106(gcm(aes)) 41049390e1e2b5d6543901daab6435f4042155fe 128",
expected: ipsecKey{
id: 3,
algo: "rfc4106(gcm(aes))",
random: "41049390e1e2b5d6543901daab6435f4042155fe",
size: 128,
},
},
}

for _, tt := range testCases {
// function to test
actual, err := ipsecKeyFromString(tt.have)

require.NoError(t, err)
require.Equal(t, tt.expected, actual)
}
}

func Test_ipsecKey_String(t *testing.T) {
key := ipsecKey{
id: 3,
algo: "rfc4106(gcm(aes))",
random: "41049390e1e2b5d6543901daab6435f4042155fe",
size: 128,
}
expected := "3 rfc4106(gcm(aes)) 41049390e1e2b5d6543901daab6435f4042155fe 128"

// function to test
actual := key.String()

require.Equal(t, expected, actual)
}

func Test_ipsecKey_rotate(t *testing.T) {
testCases := []struct {
have ipsecKey
expected ipsecKey
}{
{
have: ipsecKey{
id: 3,
algo: "rfc4106(gcm(aes))",
random: "41049390e1e2b5d6543901daab6435f4042155fe",
size: 128,
},
expected: ipsecKey{
id: 4,
algo: "rfc4106(gcm(aes))",
// this field will be randomly generated, `require.NotEqual` used for verification
random: "41049390e1e2b5d6543901daab6435f4042155fe",
size: 128,
},
},
{
have: ipsecKey{
id: 14,
algo: "rfc4106(gcm(aes))",
random: "41049390e1e2b5d6543901daab6435f4042155fe",
size: 128,
},
expected: ipsecKey{
id: 0,
algo: "rfc4106(gcm(aes))",
// this field will be randomly generated, `require.NotEqual` used for verification
random: "41049390e1e2b5d6543901daab6435f4042155fe",
size: 128,
},
},
}

for _, tt := range testCases {
// function to test
actual, err := tt.have.rotate()

require.NoError(t, err)
require.Equal(t, tt.expected.id, actual.id)
require.Equal(t, tt.expected.algo, actual.algo)
require.Equal(t, len(tt.expected.random), len(actual.random))
require.NotEqual(t, tt.expected.random, actual.random)
require.Equal(t, tt.expected.size, actual.size)
}
}
21 changes: 21 additions & 0 deletions internal/cli/cmd/encrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func newCmdEncrypt() *cobra.Command {
Aliases: []string{"encrypt"},
}
cmd.AddCommand(newCmdEncryptStatus())
cmd.AddCommand(newCmdIPsecRotateKey())
return cmd
}

Expand All @@ -47,3 +48,23 @@ func newCmdEncryptStatus() *cobra.Command {
cmd.Flags().StringVarP(&params.Output, "output", "o", status.OutputSummary, "Output format. One of: json, summary")
return cmd
}

func newCmdIPsecRotateKey() *cobra.Command {
params := encrypt.Parameters{}
cmd := &cobra.Command{
Use: "rotate-key",
Short: "Rotate IPsec key",
Long: "This command rotates IPsec encryption key in the cluster",
RunE: func(cmd *cobra.Command, args []string) error {
params.CiliumNamespace = namespace
s := encrypt.NewStatus(k8sClient, params)
if err := s.IPsecRotateKey(context.Background()); err != nil {
fatalf("Unable to rotate IPsec key: %s", err)
}
return nil
},
}
cmd.Flags().DurationVar(&params.WaitDuration, "wait-duration", 1*time.Minute, "Maximum time to wait for result, default 1 minute")
cmd.Flags().StringVarP(&params.Output, "output", "o", status.OutputSummary, "Output format. One of: json, summary")
return cmd
}

0 comments on commit 7748386

Please sign in to comment.