Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add command to consul-k8s-control-plane: gossip-encryption-autogenerate #772

Merged
merged 37 commits into from
Oct 15, 2021
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
fbdac53
Add the initial gossip-encryption-autogen command stub
Oct 4, 2021
fd7db95
Move synopsis and help to the bottom of the command file
Oct 5, 2021
db20a79
Add logging flags to init
Oct 6, 2021
f8cca95
Clean up error and logging messages
Oct 6, 2021
7130027
Add secret struct and some basic tests for it
Oct 6, 2021
b319659
Only require secret name to be set
Oct 6, 2021
fc4b912
Add test for flag validation
Oct 6, 2021
19355c9
Generate a 32 byte secret value
Oct 6, 2021
70bd3a1
Add kubernetes client to command
Oct 6, 2021
3cb2981
Write the secret to Kubernetes
Oct 6, 2021
a924567
Re-order flags
Oct 7, 2021
0b31aa1
Add required namespace flag logic
Oct 7, 2021
9ccf5cf
Test for namespace flag and log flag errors
Oct 7, 2021
ec1d795
Delete secret
Oct 7, 2021
5f61553
Secret creation and storage brought into command
Oct 7, 2021
65ac555
Safe exit if secret already exists
Oct 8, 2021
5f98102
Add context to the command
Oct 8, 2021
54e07d1
Rename k8s to k8sFlags
Oct 8, 2021
4889910
Add some nice tests
Oct 8, 2021
aa36790
Merge branch 'main' into autogen-encryption-golang
Oct 8, 2021
2a48e1d
Inline functions
Oct 11, 2021
3d78e8b
Move init to the tippy-top
Oct 11, 2021
3464eb0
Use Sprintf instead of Errorf...Error()
Oct 11, 2021
911d8fb
Move initialization of err closer to useage
Oct 11, 2021
4d3759e
Remove client check in secret exists check
Oct 11, 2021
967c334
Remove unneeded else
Oct 11, 2021
f0f47e4
Rename SafeFail to EarlyTerminationWithSuccessCode
Oct 11, 2021
915b909
Add a message on success
Oct 11, 2021
f5169dd
Test secret generation from the outside
Oct 11, 2021
0640bc9
Add changelog entry
Oct 13, 2021
c2267c8
Update CHANGELOG.md
Oct 13, 2021
600cf46
Grammar fix in changelog
Oct 13, 2021
f6edc6f
Update comments and synopsis for clarity
Oct 13, 2021
63f04a2
Clarify the does secret exists method
Oct 13, 2021
cde88f5
Rename doesK8sSecretExist to doesKubernetesSecretExist
Oct 13, 2021
22d98db
Some polish to make it sing 😘 🤌
Oct 13, 2021
5c0f64c
Update control-plane/subcommand/gossip-encryption-autogenerate/comman…
Oct 14, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}

t-eckert marked this conversation as resolved.
Show resolved Hide resolved
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
t-eckert marked this conversation as resolved.
Show resolved Hide resolved
c.UI.Info(fmt.Sprintf("A Kubernetes secret with the name `%s` already exists.", c.flagSecretName))
return 0
}

gossipSecret, err := generateGossipSecret()
if err != nil {
t-eckert marked this conversation as resolved.
Show resolved Hide resolved
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),
},
t-eckert marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
}
t-eckert marked this conversation as resolved.
Show resolved Hide resolved

// 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{})

kschoche marked this conversation as resolved.
Show resolved Hide resolved
// 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)
}