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 20 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
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
235 changes: 235 additions & 0 deletions control-plane/subcommand/gossip-encryption-autogenerate/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
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
k8sFlags *flags.K8SFlags

// flags that dictate where the Kubernetes secret will be stored
flagNamespace string
flagSecretName string
flagSecretKey string

k8sClient kubernetes.Interface

// log
log hclog.Logger
flagLogLevel string
flagLogJSON bool

once sync.Once
ctx context.Context
help string
}

// Run parses flags and runs the command.
func (c *Command) Run(args []string) int {
var err error
t-eckert marked this conversation as resolved.
Show resolved Hide resolved

c.once.Do(c.init)

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

if err = c.validateFlags(); err != nil {
c.UI.Error(fmt.Errorf("failed to validate flags: %v", err).Error())
t-eckert marked this conversation as resolved.
Show resolved Hide resolved
return 1
}

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.createK8sClient(); err != nil {
c.UI.Error(fmt.Errorf("failed to create k8s client: %v", err).Error())
t-eckert marked this conversation as resolved.
Show resolved Hide resolved
return 1
}
}

t-eckert marked this conversation as resolved.
Show resolved Hide resolved
if exists, err := c.doesK8sSecretExist(); err != nil {
t-eckert marked this conversation as resolved.
Show resolved Hide resolved
c.UI.Error(fmt.Errorf("failed to check if k8s secret exists: %v", err).Error())
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.Errorf("failed to generate gossip secret: %v", err).Error())
return 1
}

kubernetesSecret, err := c.createK8sSecret(gossipSecret)
if err != nil {
c.UI.Error(fmt.Errorf("failed to create kubernetes secret: %v", err).Error())
return 1
}
t-eckert marked this conversation as resolved.
Show resolved Hide resolved

if err = c.writeToK8s(*kubernetesSecret); err != nil {
c.UI.Error(fmt.Errorf("failed to write to k8s: %v", err).Error())
return 1
}
t-eckert marked this conversation as resolved.
Show resolved Hide resolved

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
}

// init is run once to set up usage documentation for flags.
func (c *Command) init() {
t-eckert marked this conversation as resolved.
Show resolved Hide resolved
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.k8sFlags = &flags.K8SFlags{}
flags.Merge(c.flags, c.k8sFlags.Flags())

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

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

// createK8sClient creates a Kubernetes client on the command object.
func (c *Command) createK8sClient() error {
if c.k8sFlags == nil {
return fmt.Errorf("k8s flags are nil")
}

config, err := subcommand.K8SConfig(c.k8sFlags.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

// doesK8sSecretExist checks if a secret with the given name exists in the given namespace.
func (c *Command) doesK8sSecretExist() (bool, error) {
if c.k8sClient == nil {
return false, fmt.Errorf("k8s client is not initialized")
}
t-eckert marked this conversation as resolved.
Show resolved Hide resolved

_, 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 err != nil {
if !apierrors.IsNotFound(err) {
t-eckert marked this conversation as resolved.
Show resolved Hide resolved
return false, fmt.Errorf("failed to get kubernetes secret: %v", err)
} else {
return false, nil
}
}
t-eckert marked this conversation as resolved.
Show resolved Hide resolved

return true, nil
}

// createK8sSecret creates a Kubernetes secret from the gossip secret using given secret name and key.
func (c *Command) createK8sSecret(gossipSecret string) (*v1.Secret, error) {
if (c.flagNamespace == "") || (c.flagSecretName == "") || (c.flagSecretKey == "") {
return nil, fmt.Errorf("namespace, secret name, and key must be set")
}
t-eckert marked this conversation as resolved.
Show resolved Hide resolved

return &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: c.flagSecretName,
Namespace: c.flagNamespace,
},
Data: map[string][]byte{
c.flagSecretKey: []byte(gossipSecret),
},
}, nil
}

// writeToK8s uses the Kubernetes client to write the gossip secret
// in the Kubernetes cluster at set namespace, secret name, and key.
func (c *Command) writeToK8s(secret v1.Secret) error {
if c.k8sClient == nil {
return fmt.Errorf("k8s client is not initialized")
}

_, err := c.k8sClient.CoreV1().Secrets(c.flagNamespace).Create(c.ctx, &secret, metav1.CreateOptions{})
return err
}

// Generates a random 32 byte secret.
t-eckert marked this conversation as resolved.
Show resolved Hide resolved
func generateGossipSecret() (string, error) {
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 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.
`
Loading