Skip to content

Commit

Permalink
Add command to consul-k8s-control-plane: `gossip-encryption-autogen…
Browse files Browse the repository at this point in the history
…erate` (#772)

* Add the initial gossip-encryption-autogen command stub

* Move synopsis and help to the bottom of the command file

* Add logging flags to init

* Clean up error and logging messages

* Add secret struct and some basic tests for it

* Only require secret name to be set

* Add test for flag validation

* Generate a 32 byte secret value

* Add kubernetes client to command

* Write the secret to Kubernetes

* Re-order flags

* Add required namespace flag logic

* Test for namespace flag and log flag errors

* Delete secret

* Secret creation and storage brought into command

* Safe exit if secret already exists

* Add context to the command

* Rename k8s to k8sFlags

* Add some nice tests

* Inline functions

* Move init to the tippy-top

* Use Sprintf instead of Errorf...Error()

* Move initialization of err closer to useage

* Remove client check in secret exists check

* Remove unneeded else

* Rename SafeFail to EarlyTerminationWithSuccessCode

* Add a message on success

* Test secret generation from the outside

* Add changelog entry

* Update CHANGELOG.md

Co-authored-by: Kyle Schochenmaier <kschoche@gmail.com>

* Grammar fix in changelog

* Update comments and synopsis for clarity

* Clarify the does secret exists method

* Rename doesK8sSecretExist to doesKubernetesSecretExist

* Some polish to make it sing 😘 🤌

* Update control-plane/subcommand/gossip-encryption-autogenerate/command.go

Co-authored-by: Nitya Dhanushkodi <nitya@hashicorp.com>

Co-authored-by: Kyle Schochenmaier <kschoche@gmail.com>
Co-authored-by: Nitya Dhanushkodi <nitya@hashicorp.com>
  • Loading branch information
3 people authored Oct 15, 2021
1 parent 56cbbb0 commit 00082a0
Show file tree
Hide file tree
Showing 4 changed files with 321 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## UNRELEASED

FEATURES:
* Control Plane
* Add `gossip-encryption-autogenerate` subcommand to generate a random 32 byte Kubernetes secret to be used as a gossip encryption key. [[GH-772](https://github.com/hashicorp/consul-k8s/pull/772)]
* Helm Chart
* Add automatic generation of gossip encryption with `global.gossipEncryption.autoGenerate=true`. [[GH-738](https://github.com/hashicorp/consul-k8s/pull/738)]
* Add support for configuring resources for mesh gateway `service-init` container. [[GH-758](https://github.com/hashicorp/consul-k8s/pull/758)]
Expand Down
5 changes: 5 additions & 0 deletions control-plane/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
cmdCreateFederationSecret "github.com/hashicorp/consul-k8s/control-plane/subcommand/create-federation-secret"
cmdDeleteCompletedJob "github.com/hashicorp/consul-k8s/control-plane/subcommand/delete-completed-job"
cmdGetConsulClientCA "github.com/hashicorp/consul-k8s/control-plane/subcommand/get-consul-client-ca"
cmdGossipEncryptionAutogenerate "github.com/hashicorp/consul-k8s/control-plane/subcommand/gossip-encryption-autogenerate"
cmdInjectConnect "github.com/hashicorp/consul-k8s/control-plane/subcommand/inject-connect"
cmdPartitionInit "github.com/hashicorp/consul-k8s/control-plane/subcommand/partition-init"
cmdServerACLInit "github.com/hashicorp/consul-k8s/control-plane/subcommand/server-acl-init"
Expand Down Expand Up @@ -88,6 +89,10 @@ func init() {
"tls-init": func() (cli.Command, error) {
return &cmdTLSInit.Command{UI: ui}, nil
},

"gossip-encryption-autogenerate": func() (cli.Command, error) {
return &cmdGossipEncryptionAutogenerate.Command{UI: ui}, nil
},
}
}

Expand Down
211 changes: 211 additions & 0 deletions control-plane/subcommand/gossip-encryption-autogenerate/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package gossipencryptionautogenerate

import (
"context"
"crypto/rand"
"encoding/base64"
"flag"
"fmt"
"sync"

"github.com/hashicorp/consul-k8s/control-plane/subcommand"
"github.com/hashicorp/consul-k8s/control-plane/subcommand/common"
"github.com/hashicorp/consul-k8s/control-plane/subcommand/flags"
"github.com/hashicorp/go-hclog"
"github.com/mitchellh/cli"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)

type Command struct {
UI cli.Ui

flags *flag.FlagSet
k8s *flags.K8SFlags

// These flags determine where the Kubernetes secret will be stored.
flagNamespace string
flagSecretName string
flagSecretKey string

flagLogLevel string
flagLogJSON bool

k8sClient kubernetes.Interface

log hclog.Logger
once sync.Once
ctx context.Context
help string
}

// init is run once to set up usage documentation for flags.
func (c *Command) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)

c.flags.StringVar(&c.flagLogLevel, "log-level", "info",
"Log verbosity level. Supported values (in order of detail) are \"trace\", "+
"\"debug\", \"info\", \"warn\", and \"error\".")
c.flags.BoolVar(&c.flagLogJSON, "log-json", false, "Enable or disable JSON output format for logging.")
c.flags.StringVar(&c.flagNamespace, "namespace", "", "Name of Kubernetes namespace where Consul and consul-k8s components are deployed.")
c.flags.StringVar(&c.flagSecretName, "secret-name", "", "Name of the secret to create.")
c.flags.StringVar(&c.flagSecretKey, "secret-key", "key", "Name of the secret key to create.")

c.k8s = &flags.K8SFlags{}
flags.Merge(c.flags, c.k8s.Flags())

c.help = flags.Usage(help, c.flags)
}

// Run parses input and creates a gossip secret in Kubernetes if none exists at the given namespace and secret name.
func (c *Command) Run(args []string) int {
c.once.Do(c.init)

if err := c.flags.Parse(args); err != nil {
c.UI.Error(fmt.Sprintf("Failed to parse args: %v", err))
return 1
}

if err := c.validateFlags(); err != nil {
c.UI.Error(fmt.Sprintf("Failed to validate flags: %v", err))
return 1
}

var err error
c.log, err = common.Logger(c.flagLogLevel, c.flagLogJSON)
if err != nil {
c.UI.Error(err.Error())
return 1
}

if c.ctx == nil {
c.ctx = context.Background()
}

if c.k8sClient == nil {
if err = c.createKubernetesClient(); err != nil {
c.UI.Error(fmt.Sprintf("Failed to create Kubernetes client: %v", err))
return 1
}
}

if exists, err := c.doesKubernetesSecretExist(); err != nil {
c.UI.Error(fmt.Sprintf("Failed to check if Kubernetes secret exists: %v", err))
return 1
} else if exists {
// Safe exit if secret already exists.
c.UI.Info(fmt.Sprintf("A Kubernetes secret with the name `%s` already exists.", c.flagSecretName))
return 0
}

gossipSecret, err := generateGossipSecret()
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to generate gossip secret: %v", err))
return 1
}

// Create the Kubernetes secret object.
kubernetesSecret := v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: c.flagSecretName,
Namespace: c.flagNamespace,
},
Data: map[string][]byte{
c.flagSecretKey: []byte(gossipSecret),
},
}

// Write the secret to Kubernetes.
_, err = c.k8sClient.CoreV1().Secrets(c.flagNamespace).Create(c.ctx, &kubernetesSecret, metav1.CreateOptions{})
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to create Kubernetes secret: %v", err))
return 1
}

c.UI.Info(fmt.Sprintf("Successfully created Kubernetes secret `%s` in namespace `%s`.", c.flagSecretName, c.flagNamespace))
return 0
}

// Help returns the command's help text.
func (c *Command) Help() string {
c.once.Do(c.init)
return c.help
}

// Synopsis returns a one-line synopsis of the command.
func (c *Command) Synopsis() string {
return synopsis
}

// validateFlags ensures that all required flags are set.
func (c *Command) validateFlags() error {
if c.flagNamespace == "" {
return fmt.Errorf("-namespace must be set")
}

if c.flagSecretName == "" {
return fmt.Errorf("-secret-name must be set")
}

return nil
}

// createKubernetesClient creates a Kubernetes client on the command object.
func (c *Command) createKubernetesClient() error {
config, err := subcommand.K8SConfig(c.k8s.KubeConfig())
if err != nil {
return fmt.Errorf("failed to create Kubernetes config: %v", err)
}

c.k8sClient, err = kubernetes.NewForConfig(config)
if err != nil {
return fmt.Errorf("error initializing Kubernetes client: %s", err)
}

return nil
}

// doesKubernetesSecretExist checks if a secret with the given name exists in the given namespace.
func (c *Command) doesKubernetesSecretExist() (bool, error) {
_, err := c.k8sClient.CoreV1().Secrets(c.flagNamespace).Get(c.ctx, c.flagSecretName, metav1.GetOptions{})

// If the secret does not exist, the error will be a NotFound error.
if err != nil && apierrors.IsNotFound(err) {
return false, nil
}

// If the error is not a NotFound error, return the error.
if err != nil && !apierrors.IsNotFound(err) {
return false, fmt.Errorf("failed to get Kubernetes secret: %v", err)
}

// The secret exists.
return true, nil
}

// generateGossipSecret generates a random 32 byte secret returned as a base64 encoded string.
func generateGossipSecret() (string, error) {
// This code was copied from Consul's Keygen command:
// https://github.com/hashicorp/consul/blob/d652cc86e3d0322102c2b5e9026c6a60f36c17a5/command/keygen/keygen.go

key := make([]byte, 32)
n, err := rand.Reader.Read(key)

if err != nil {
return "", fmt.Errorf("error reading random data: %s", err)
}
if n != 32 {
return "", fmt.Errorf("couldn't read enough entropy")
}

return base64.StdEncoding.EncodeToString(key), nil
}

const synopsis = "Generate and store a secret for gossip encryption."
const help = `
Usage: consul-k8s-control-plane gossip-encryption-autogenerate [options]
Bootstraps the installation with a secret for gossip encryption.
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package gossipencryptionautogenerate

import (
"context"
"encoding/base64"
"fmt"
"testing"

"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)

func TestRun_FlagValidation(t *testing.T) {
t.Parallel()
cases := []struct {
flags []string
expErr string
}{
{
flags: []string{},
expErr: "-namespace must be set",
},
{
flags: []string{"-namespace", "default"},
expErr: "-secret-name must be set",
},
{
flags: []string{"-namespace", "default", "-secret-name", "my-secret", "-log-level", "oak"},
expErr: "unknown log level",
},
}

for _, c := range cases {
t.Run(c.expErr, func(t *testing.T) {
ui := cli.NewMockUi()
cmd := Command{
UI: ui,
}
code := cmd.Run(c.flags)
require.Equal(t, 1, code)
require.Contains(t, ui.ErrorWriter.String(), c.expErr)
})
}
}

func TestRun_EarlyTerminationWithSuccessCodeIfSecretExists(t *testing.T) {
namespace := "default"
secretName := "my-secret"
secretKey := "my-secret-key"

ui := cli.NewMockUi()
k8s := fake.NewSimpleClientset()

cmd := Command{UI: ui, k8sClient: k8s}

// Create a secret.
secret := v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
Data: map[string][]byte{
secretKey: []byte(secretKey),
},
}
_, err := k8s.CoreV1().Secrets(namespace).Create(context.Background(), &secret, metav1.CreateOptions{})
require.NoError(t, err)

// Run the command.
flags := []string{"-namespace", namespace, "-secret-name", secretName, "-secret-key", secretKey}
code := cmd.Run(flags)

require.Equal(t, 0, code)
require.Contains(t, ui.OutputWriter.String(), fmt.Sprintf("A Kubernetes secret with the name `%s` already exists.", secretName))
}

func TestRun_SecretIsGeneratedIfNoneExists(t *testing.T) {
namespace := "default"
secretName := "my-secret"
secretKey := "my-secret-key"

ui := cli.NewMockUi()
k8s := fake.NewSimpleClientset()

cmd := Command{UI: ui, k8sClient: k8s}

// Run the command.
flags := []string{"-namespace", namespace, "-secret-name", secretName, "-secret-key", secretKey}
code := cmd.Run(flags)

require.Equal(t, 0, code)
require.Contains(t, ui.OutputWriter.String(), fmt.Sprintf("Successfully created Kubernetes secret `%s` in namespace `%s`.", secretName, namespace))

// Check the secret was created.
secret, err := k8s.CoreV1().Secrets(namespace).Get(context.Background(), secretName, metav1.GetOptions{})
require.NoError(t, err)
gossipSecret, err := base64.StdEncoding.DecodeString(string(secret.Data[secretKey]))
require.NoError(t, err)
require.Len(t, gossipSecret, 32)
}

0 comments on commit 00082a0

Please sign in to comment.