Merge pull request #11822 from johngmyers/rotate-issue

Support creating new service-account keypairs
This commit is contained in:
Kubernetes Prow Robot 2021-06-21 01:32:59 -07:00 committed by GitHub
commit 21488a164d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 197 additions and 275 deletions

1
cmd/kops/BUILD.bazel generated
View File

@ -8,7 +8,6 @@ go_library(
"create_cluster.go",
"create_ig.go",
"create_keypair.go",
"create_keypair_ca.go",
"create_secret.go",
"create_secret_cilium_encryptionconfig.go",
"create_secret_dockerconfig.go",

View File

@ -1,5 +1,5 @@
/*
Copyright 2021 The Kubernetes Authors.
Copyright 2019 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.
@ -17,38 +17,188 @@ limitations under the License.
package main
import (
"context"
"crypto/x509/pkix"
"fmt"
"io"
"io/ioutil"
"os"
"time"
"github.com/spf13/cobra"
"k8s.io/klog/v2"
"k8s.io/kops/cmd/kops/util"
"k8s.io/kops/pkg/pki"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/utils"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
)
var (
createKeypairLong = templates.LongDesc(i18n.T(`
Create a keypair`))
Add a CA certificate and private key to a keyset.
`))
createKeypairExample = templates.Examples(i18n.T(`
Add a cluster CA certificate and private key.
Add a CA certificate and private key to a keyset.
kops create keypair ca \
--cert ~/ca.pem --key ~/ca-key.pem \
--name k8s-cluster.example.com --state s3://my-state-store
`))
createKeypairShort = i18n.T(`Create a keypair.`)
createKeypairShort = i18n.T(`Add a CA certificate and private key to a keyset.`)
)
type CreateKeypairOptions struct {
ClusterName string
Keyset string
PrivateKeyPath string
CertPath string
Primary bool
}
var keysetCommonNames = map[string]string{
"ca": "kubernetes",
"service-account": "service-account",
}
// NewCmdCreateKeypair returns a create keypair command.
func NewCmdCreateKeypair(f *util.Factory, out io.Writer) *cobra.Command {
options := &CreateKeypairOptions{}
cmd := &cobra.Command{
Use: "keypair",
Use: "keypair KEYSET",
Short: createKeypairShort,
Long: createKeypairLong,
Example: createKeypairExample,
Run: func(cmd *cobra.Command, args []string) {
ctx := context.TODO()
options.ClusterName = rootCommand.ClusterName()
if options.ClusterName == "" {
exitWithError(fmt.Errorf("--name is required"))
return
}
if len(args) == 0 {
exitWithError(fmt.Errorf("must specify name of keyset to add keypair to"))
}
if len(args) != 1 {
exitWithError(fmt.Errorf("can only add to one keyset at a time"))
}
options.Keyset = args[0]
err := RunCreateKeypair(ctx, f, out, options)
if err != nil {
exitWithError(err)
}
},
}
// create subcommands
cmd.AddCommand(NewCmdCreateKeypairCa(f, out))
cmd.Flags().StringVar(&options.CertPath, "cert", options.CertPath, "Path to CA certificate")
cmd.Flags().StringVar(&options.PrivateKeyPath, "key", options.PrivateKeyPath, "Path to CA private key")
cmd.Flags().BoolVar(&options.Primary, "primary", options.Primary, "Make the keypair the one used to issue certificates")
return cmd
}
// RunCreateKeypair adds a custom CA certificate and private key.
func RunCreateKeypair(ctx context.Context, f *util.Factory, out io.Writer, options *CreateKeypairOptions) error {
commonName := keysetCommonNames[options.Keyset]
if commonName == "" {
return fmt.Errorf("adding keypair to %q is not supported", options.Keyset)
}
cluster, err := GetCluster(ctx, f, options.ClusterName)
if err != nil {
return fmt.Errorf("error getting cluster: %q: %v", options.ClusterName, err)
}
clientSet, err := f.Clientset()
if err != nil {
return fmt.Errorf("error getting clientset: %v", err)
}
keyStore, err := clientSet.KeyStore(cluster)
if err != nil {
return fmt.Errorf("error getting keystore: %v", err)
}
var privateKey *pki.PrivateKey
if options.PrivateKeyPath != "" {
options.PrivateKeyPath = utils.ExpandPath(options.PrivateKeyPath)
privateKeyBytes, err := ioutil.ReadFile(options.PrivateKeyPath)
if err != nil {
return fmt.Errorf("error reading user provided private key %q: %v", options.PrivateKeyPath, err)
}
privateKey, err = pki.ParsePEMPrivateKey(privateKeyBytes)
if err != nil {
return fmt.Errorf("error loading private key %q: %v", privateKeyBytes, err)
}
}
var cert *pki.Certificate
if options.CertPath == "" {
if privateKey == nil {
privateKey, err = pki.GeneratePrivateKey()
if err != nil {
return fmt.Errorf("error generating private key: %v", err)
}
}
req := pki.IssueCertRequest{
Type: "ca",
Subject: pkix.Name{CommonName: "cn=" + commonName},
Serial: pki.BuildPKISerial(time.Now().UnixNano()),
PrivateKey: privateKey,
}
cert, _, _, err = pki.IssueCert(&req, nil)
if err != nil {
return fmt.Errorf("error issuing certificate: %v", err)
}
} else {
options.CertPath = utils.ExpandPath(options.CertPath)
certBytes, err := ioutil.ReadFile(options.CertPath)
if err != nil {
return fmt.Errorf("error reading user provided cert %q: %v", options.CertPath, err)
}
cert, err = pki.ParsePEMCertificate(certBytes)
if err != nil {
return fmt.Errorf("error loading certificate %q: %v", options.CertPath, err)
}
}
keyset, err := keyStore.FindKeyset(options.Keyset)
if os.IsNotExist(err) || (err == nil && keyset == nil) {
if options.Primary {
keyset, err = fi.NewKeyset(cert, privateKey)
} else {
return fmt.Errorf("the first keypair added to a keyset must be primary")
}
} else if err != nil {
return fmt.Errorf("reading existing keyset: %v", err)
} else {
err = keyset.AddItem(cert, privateKey, options.Primary)
}
if err != nil {
return err
}
err = keyStore.StoreKeyset(options.Keyset, keyset)
if err != nil {
return fmt.Errorf("error storing user provided keys %q %q: %v", options.CertPath, options.PrivateKeyPath, err)
}
if options.CertPath != "" {
klog.Infof("using user provided cert: %v\n", options.CertPath)
}
if options.PrivateKeyPath != "" {
klog.Infof("using user provided private key: %v\n", options.PrivateKeyPath)
}
return nil
}

View File

@ -1,183 +0,0 @@
/*
Copyright 2019 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 main
import (
"context"
"crypto/x509/pkix"
"fmt"
"io"
"io/ioutil"
"os"
"time"
"github.com/spf13/cobra"
"k8s.io/klog/v2"
"k8s.io/kops/cmd/kops/util"
"k8s.io/kops/pkg/pki"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/utils"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
)
var (
createKeypairCaLong = templates.LongDesc(i18n.T(`
Add a cluster CA certificate and private key.
`))
createKeypairCaExample = templates.Examples(i18n.T(`
Add a cluster CA certificate and private key.
kops create keypair ca \
--cert ~/ca.pem --key ~/ca-key.pem \
--name k8s-cluster.example.com --state s3://my-state-store
`))
createKeypairCaShort = i18n.T(`Add a cluster CA cert and key`)
)
type CreateKeypairCaOptions struct {
ClusterName string
PrivateKeyPath string
CertPath string
Primary bool
}
// NewCmdCreateKeypairCa returns create ca certificate command
func NewCmdCreateKeypairCa(f *util.Factory, out io.Writer) *cobra.Command {
options := &CreateKeypairCaOptions{}
cmd := &cobra.Command{
Use: "ca",
Short: createKeypairCaShort,
Long: createKeypairCaLong,
Example: createKeypairCaExample,
Run: func(cmd *cobra.Command, args []string) {
ctx := context.TODO()
err := rootCommand.ProcessArgs(args)
if err != nil {
exitWithError(err)
}
options.ClusterName = rootCommand.ClusterName()
err = RunCreateKeypairCa(ctx, f, out, options)
if err != nil {
exitWithError(err)
}
},
}
cmd.Flags().StringVar(&options.CertPath, "cert", options.CertPath, "Path to CA certificate")
cmd.Flags().StringVar(&options.PrivateKeyPath, "key", options.PrivateKeyPath, "Path to CA private key")
cmd.Flags().BoolVar(&options.Primary, "primary", options.Primary, "Make the CA used to issue certificates")
return cmd
}
// RunCreateKeypairCa adds a custom ca certificate and private key
func RunCreateKeypairCa(ctx context.Context, f *util.Factory, out io.Writer, options *CreateKeypairCaOptions) error {
cluster, err := GetCluster(ctx, f, options.ClusterName)
if err != nil {
return fmt.Errorf("error getting cluster: %q: %v", options.ClusterName, err)
}
clientSet, err := f.Clientset()
if err != nil {
return fmt.Errorf("error getting clientset: %v", err)
}
keyStore, err := clientSet.KeyStore(cluster)
if err != nil {
return fmt.Errorf("error getting keystore: %v", err)
}
var privateKey *pki.PrivateKey
if options.PrivateKeyPath != "" {
options.PrivateKeyPath = utils.ExpandPath(options.PrivateKeyPath)
privateKeyBytes, err := ioutil.ReadFile(options.PrivateKeyPath)
if err != nil {
return fmt.Errorf("error reading user provided private key %q: %v", options.PrivateKeyPath, err)
}
privateKey, err = pki.ParsePEMPrivateKey(privateKeyBytes)
if err != nil {
return fmt.Errorf("error loading private key %q: %v", privateKeyBytes, err)
}
}
var cert *pki.Certificate
if options.CertPath == "" {
if privateKey == nil {
privateKey, err = pki.GeneratePrivateKey()
if err != nil {
return fmt.Errorf("error generating private key: %v", err)
}
}
req := pki.IssueCertRequest{
Type: "ca",
Subject: pkix.Name{CommonName: "cn=kubernetes"},
Serial: pki.BuildPKISerial(time.Now().UnixNano()),
PrivateKey: privateKey,
}
cert, _, _, err = pki.IssueCert(&req, nil)
if err != nil {
return fmt.Errorf("error issuing certificate: %v", err)
}
} else {
options.CertPath = utils.ExpandPath(options.CertPath)
certBytes, err := ioutil.ReadFile(options.CertPath)
if err != nil {
return fmt.Errorf("error reading user provided cert %q: %v", options.CertPath, err)
}
cert, err = pki.ParsePEMCertificate(certBytes)
if err != nil {
return fmt.Errorf("error loading certificate %q: %v", options.CertPath, err)
}
}
keyset, err := keyStore.FindKeyset(fi.CertificateIDCA)
if os.IsNotExist(err) || (err == nil && keyset == nil) {
keyset = &fi.Keyset{
Items: map[string]*fi.KeysetItem{},
}
} else if err != nil {
return fmt.Errorf("reading existing keyset: %v", err)
}
err = keyset.AddItem(cert, privateKey, options.Primary)
if err != nil {
return err
}
err = keyStore.StoreKeyset(fi.CertificateIDCA, keyset)
if err != nil {
return fmt.Errorf("error storing user provided keys %q %q: %v", options.CertPath, options.PrivateKeyPath, err)
}
if options.CertPath != "" {
klog.Infof("using user provided cert: %v\n", options.CertPath)
}
if options.PrivateKeyPath != "" {
klog.Infof("using user provided private key: %v\n", options.PrivateKeyPath)
}
return nil
}

View File

@ -41,6 +41,7 @@ import (
"k8s.io/kops/pkg/jsonutils"
"k8s.io/kops/pkg/testutils"
"k8s.io/kops/pkg/testutils/golden"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/cloudup"
"k8s.io/kops/upup/pkg/fi/cloudup/gce"
@ -480,25 +481,27 @@ func (i *integrationTest) runTest(t *testing.T, h *testutils.IntegrationTestHarn
}
{
options := &CreateKeypairCaOptions{}
options := &CreateKeypairOptions{}
options.ClusterName = i.clusterName
options.Keyset = fi.CertificateIDCA
options.PrivateKeyPath = path.Join(i.srcDir, "../ca.key")
options.CertPath = path.Join(i.srcDir, "../ca.crt")
options.Primary = true
err := RunCreateKeypairCa(ctx, factory, &stdout, options)
err := RunCreateKeypair(ctx, factory, &stdout, options)
if err != nil {
t.Fatalf("error running %q create CA keypair: %v", inputYAML, err)
}
}
{
options := &CreateKeypairCaOptions{}
options := &CreateKeypairOptions{}
options.ClusterName = i.clusterName
options.Keyset = fi.CertificateIDCA
options.PrivateKeyPath = path.Join(i.srcDir, "../ca-next.key")
options.CertPath = path.Join(i.srcDir, "../ca-next.crt")
options.Primary = false
err := RunCreateKeypairCa(ctx, factory, &stdout, options)
err := RunCreateKeypair(ctx, factory, &stdout, options)
if err != nil {
t.Fatalf("error running %q create next CA keypair: %v", inputYAML, err)
}
@ -765,25 +768,27 @@ func (i *integrationTest) runTestCloudformation(t *testing.T) {
}
{
options := &CreateKeypairCaOptions{}
options := &CreateKeypairOptions{}
options.ClusterName = i.clusterName
options.Keyset = fi.CertificateIDCA
options.PrivateKeyPath = path.Join(i.srcDir, "../ca.key")
options.CertPath = path.Join(i.srcDir, "../ca.crt")
options.Primary = true
err := RunCreateKeypairCa(ctx, factory, &stdout, options)
err := RunCreateKeypair(ctx, factory, &stdout, options)
if err != nil {
t.Fatalf("error running %q create CA keypair: %v", inputYAML, err)
}
}
{
options := &CreateKeypairCaOptions{}
options := &CreateKeypairOptions{}
options.ClusterName = i.clusterName
options.Keyset = fi.CertificateIDCA
options.PrivateKeyPath = path.Join(i.srcDir, "../ca-next.key")
options.CertPath = path.Join(i.srcDir, "../ca-next.crt")
options.Primary = false
err := RunCreateKeypairCa(ctx, factory, &stdout, options)
err := RunCreateKeypair(ctx, factory, &stdout, options)
if err != nil {
t.Fatalf("error running %q create next CA keypair: %v", inputYAML, err)
}

View File

@ -79,6 +79,6 @@ kops create -f FILENAME [flags]
* [kops](kops.md) - kOps is Kubernetes Operations.
* [kops create cluster](kops_create_cluster.md) - Create a Kubernetes cluster.
* [kops create instancegroup](kops_create_instancegroup.md) - Create an instancegroup.
* [kops create keypair](kops_create_keypair.md) - Create a keypair.
* [kops create keypair](kops_create_keypair.md) - Add a CA certificate and private key to a keyset.
* [kops create secret](kops_create_secret.md) - Create a secret.

View File

@ -3,16 +3,20 @@
## kops create keypair
Create a keypair.
Add a CA certificate and private key to a keyset.
### Synopsis
Create a keypair
Add a CA certificate and private key to a keyset.
```
kops create keypair KEYSET [flags]
```
### Examples
```
Add a cluster CA certificate and private key.
Add a CA certificate and private key to a keyset.
kops create keypair ca \
--cert ~/ca.pem --key ~/ca-key.pem \
--name k8s-cluster.example.com --state s3://my-state-store
@ -21,7 +25,10 @@ Create a keypair
### Options
```
-h, --help help for keypair
--cert string Path to CA certificate
-h, --help help for keypair
--key string Path to CA private key
--primary Make the keypair the one used to issue certificates
```
### Options inherited from parent commands
@ -48,5 +55,4 @@ Create a keypair
### SEE ALSO
* [kops create](kops_create.md) - Create a resource by command line, filename or stdin.
* [kops create keypair ca](kops_create_keypair_ca.md) - Add a cluster CA cert and key

View File

@ -1,58 +0,0 @@
<!--- This file is automatically generated by make gen-cli-docs; changes should be made in the go CLI command code (under cmd/kops) -->
## kops create keypair ca
Add a cluster CA cert and key
### Synopsis
Add a cluster CA certificate and private key.
```
kops create keypair ca [flags]
```
### Examples
```
Add a cluster CA certificate and private key.
kops create keypair ca \
--cert ~/ca.pem --key ~/ca-key.pem \
--name k8s-cluster.example.com --state s3://my-state-store
```
### Options
```
--cert string Path to CA certificate
-h, --help help for ca
--key string Path to CA private key
--primary Make the CA used to issue certificates
```
### Options inherited from parent commands
```
--add_dir_header If true, adds the file directory to the header of the log messages
--alsologtostderr log to standard error as well as files
--config string yaml config file (default is $HOME/.kops.yaml)
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
--log_dir string If non-empty, write log files in this directory
--log_file string If non-empty, use this log file
--log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800)
--logtostderr log to standard error instead of files (default true)
--name string Name of cluster. Overrides KOPS_CLUSTER_NAME environment variable
--one_output If true, only write logs to their native severity level (vs also writing to each lower severity level)
--skip_headers If true, avoid header prefixes in the log messages
--skip_log_headers If true, avoid headers when opening log files
--state string Location of state storage (kops 'config' file). Overrides KOPS_STATE_STORE environment variable
--stderrthreshold severity logs at or above this threshold go to stderr (default 2)
-v, --v Level number for the log level verbosity
--vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging
```
### SEE ALSO
* [kops create keypair](kops_create_keypair.md) - Create a keypair.

View File

@ -132,18 +132,8 @@ func buildMinimalCluster(clusterName string, masterPublicName string, lbCert boo
func fakeKeyset() *fi.Keyset {
cert, _ := pki.ParsePEMCertificate([]byte(certData))
key, _ := pki.ParsePEMPrivateKey([]byte(privatekeyData))
keysetItem := &fi.KeysetItem{
Id: "99",
Certificate: cert,
PrivateKey: key,
}
return &fi.Keyset{
LegacyFormat: true,
Items: map[string]*fi.KeysetItem{
"99": keysetItem,
},
Primary: keysetItem,
}
keyset, _ := fi.NewKeyset(cert, key)
return keyset
}
func TestBuildKubecfg(t *testing.T) {

View File

@ -228,6 +228,19 @@ func (k *Keyset) ToPublicKeyBytes() ([]byte, error) {
return buf.Bytes(), nil
}
// NewKeyset creates a Keyset.
func NewKeyset(cert *pki.Certificate, privateKey *pki.PrivateKey) (*Keyset, error) {
keyset := &Keyset{
Items: map[string]*KeysetItem{},
}
err := keyset.AddItem(cert, privateKey, true)
if err != nil {
return nil, err
}
return keyset, nil
}
// AddItem adds an item to the keyset
func (k *Keyset) AddItem(cert *pki.Certificate, privateKey *pki.PrivateKey, primary bool) error {
if cert == nil {