diff --git a/cmd/kops/create_cluster.go b/cmd/kops/create_cluster.go index 11bda25cde..77ec065856 100644 --- a/cmd/kops/create_cluster.go +++ b/cmd/kops/create_cluster.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/golang/glog" "github.com/spf13/cobra" + "io/ioutil" "k8s.io/kops/upup/pkg/api" "k8s.io/kops/upup/pkg/fi" "k8s.io/kops/upup/pkg/fi/cloudup" @@ -112,6 +113,17 @@ func (c *CreateClusterCmd) Run(args []string) error { c.OutDir = "out" } + var sshPublicKeyData []byte + if c.SSHPublicKey != "" { + sshPublicKey := c.SSHPublicKey + sshPublicKey = utils.ExpandPath(sshPublicKey) + authorized, err := ioutil.ReadFile(sshPublicKey) + if err != nil { + return fmt.Errorf("error reading SSH key file %q: %v", sshPublicKey, err) + } + sshPublicKeyData = authorized + } + clusterRegistry, err := rootCommand.ClusterRegistry() if err != nil { return err @@ -297,10 +309,6 @@ func (c *CreateClusterCmd) Run(args []string) error { } } - if c.SSHPublicKey != "" { - c.SSHPublicKey = utils.ExpandPath(c.SSHPublicKey) - } - if c.AdminAccess != "" { cluster.Spec.AdminAccess = []string{c.AdminAccess} } @@ -345,6 +353,14 @@ func (c *CreateClusterCmd) Run(args []string) error { return fmt.Errorf("error writing updated configuration: %v", err) } + if sshPublicKeyData != nil { + keystore := clusterRegistry.KeyStore(clusterName) + err := keystore.AddSSHPublicKey("admin", sshPublicKeyData) + if err != nil { + return fmt.Errorf("error storing SSH public key: %v", err) + } + } + err = clusterRegistry.WriteCompletedConfig(fullCluster) if err != nil { return fmt.Errorf("error writing completed cluster spec: %v", err) @@ -360,7 +376,6 @@ func (c *CreateClusterCmd) Run(args []string) error { Models: strings.Split(c.Models, ","), ClusterRegistry: clusterRegistry, Target: c.Target, - SSHPublicKey: c.SSHPublicKey, OutDir: c.OutDir, DryRun: isDryrun, } diff --git a/cmd/kops/secrets_create.go b/cmd/kops/secrets_create.go index 1d53cd48d5..22437e34e9 100644 --- a/cmd/kops/secrets_create.go +++ b/cmd/kops/secrets_create.go @@ -6,6 +6,7 @@ import ( "crypto/x509" "github.com/golang/glog" "github.com/spf13/cobra" + "k8s.io/kops/upup/pkg/fi" "net" "strings" ) @@ -62,9 +63,13 @@ func (cmd *CreateSecretsCommand) Run() error { if err != nil { return err } - _, created, err := secretStore.GetOrCreateSecret(cmd.Id) + secret, err := fi.CreateSecret() if err != nil { - return fmt.Errorf("error creating secrets %v", err) + return fmt.Errorf("error creating secret: %v", err) + } + _, created, err := secretStore.GetOrCreateSecret(cmd.Id, secret) + if err != nil { + return fmt.Errorf("error creating secret: %v", err) } if !created { return fmt.Errorf("secret already exists") diff --git a/cmd/kops/update_cluster.go b/cmd/kops/update_cluster.go index eb6b439ce6..d04194037e 100644 --- a/cmd/kops/update_cluster.go +++ b/cmd/kops/update_cluster.go @@ -6,17 +6,15 @@ import ( "github.com/spf13/cobra" "k8s.io/kops/upup/pkg/api" "k8s.io/kops/upup/pkg/fi/cloudup" - "k8s.io/kops/upup/pkg/fi/utils" "k8s.io/kops/upup/pkg/kutil" "strings" ) type UpdateClusterCmd struct { - Yes bool - Target string - Models string - OutDir string - SSHPublicKey string + Yes bool + Target string + Models string + OutDir string } var updateCluster UpdateClusterCmd @@ -39,7 +37,6 @@ func init() { cmd.Flags().BoolVar(&updateCluster.Yes, "yes", false, "Actually create cloud resources") cmd.Flags().StringVar(&updateCluster.Target, "target", "direct", "Target - direct, terraform") cmd.Flags().StringVar(&updateCluster.Models, "model", "config,proto,cloudup", "Models to apply (separate multiple models with commas)") - cmd.Flags().StringVar(&updateCluster.SSHPublicKey, "ssh-public-key", "~/.ssh/id_rsa.pub", "SSH public key to use") cmd.Flags().StringVar(&updateCluster.OutDir, "out", "", "Path to write any local output") } @@ -86,10 +83,6 @@ func (c *UpdateClusterCmd) Run(args []string) error { return err } - if c.SSHPublicKey != "" { - c.SSHPublicKey = utils.ExpandPath(c.SSHPublicKey) - } - strict := false err = api.DeepValidate(cluster, fullInstanceGroups, strict) if err != nil { @@ -102,7 +95,6 @@ func (c *UpdateClusterCmd) Run(args []string) error { Models: strings.Split(c.Models, ","), ClusterRegistry: clusterRegistry, Target: c.Target, - SSHPublicKey: c.SSHPublicKey, OutDir: c.OutDir, DryRun: isDryrun, } diff --git a/docs/arguments.md b/docs/arguments.md index 4c8ce7278a..f65c532efc 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -1,5 +1,13 @@ # Detailed description of arguments +# create + +## ssh-public-key + +`--ssh-public-key` is the path to the SSH public key to set up on the cluster. (The SSH username is admin) + +This will automatically create an SSH public-key in the keystore (`kubectl get secret`) with a name of `admin`. + ## admin-access `admin-access` controls the CIDR which can access the admin endpoints (SSH to each node, HTTPS to the master). diff --git a/upup/pkg/fi/ca.go b/upup/pkg/fi/ca.go index 68c4435c4c..62ad40f4f0 100644 --- a/upup/pkg/fi/ca.go +++ b/upup/pkg/fi/ca.go @@ -28,6 +28,13 @@ type Certificate struct { PublicKey crypto.PublicKey } +type KeystoreItem struct { + Type string + Name string + Id string + Data []byte +} + func (c *Certificate) UnmarshalJSON(b []byte) error { s := "" if err := json.Unmarshal(b, &s); err == nil { @@ -87,6 +94,12 @@ type CAStore interface { // AddCert adds an alternative certificate to the pool (primarily useful for CAs) AddCert(id string, cert *Certificate) error + + // AddSSHPublicKey adds an SSH public key + AddSSHPublicKey(id string, data []byte) error + + // FindSSHPublicKeys retrieves the SSH public keys with the specific name + FindSSHPublicKeys(name string) ([]*KeystoreItem, error) } func (c *Certificate) AsString() (string, error) { diff --git a/upup/pkg/fi/cloudup/apply_cluster.go b/upup/pkg/fi/cloudup/apply_cluster.go index 212a2dfd7d..60197be711 100644 --- a/upup/pkg/fi/cloudup/apply_cluster.go +++ b/upup/pkg/fi/cloudup/apply_cluster.go @@ -3,7 +3,6 @@ package cloudup import ( "fmt" "github.com/golang/glog" - "io/ioutil" "k8s.io/kops/upup/pkg/api" "k8s.io/kops/upup/pkg/fi" "k8s.io/kops/upup/pkg/fi/cloudup/awstasks" @@ -35,8 +34,7 @@ type ApplyClusterCmd struct { Target string //// The node model to use //NodeModel string - // The SSH public key (file) to use - SSHPublicKey string + // OutDir is a local directory in which we place output, can cache files etc OutDir string @@ -235,11 +233,23 @@ func (c *ApplyClusterCmd) Run() error { "dnsZone": &awstasks.DNSZone{}, }) - if c.SSHPublicKey == "" { - return fmt.Errorf("SSH public key must be specified when running with AWS") + l.TemplateFunctions["MachineTypeInfo"] = awsup.GetMachineTypeInfo + + { + sshPublicKeys, err := keyStore.FindSSHPublicKeys("admin") + if err != nil { + return fmt.Errorf("error reading SSH public key %q: %v", "admin", err) + } + if len(sshPublicKeys) == 0 { + return fmt.Errorf("Must specify SSH public key when running with AWS") + } + if len(sshPublicKeys) != 1 { + glog.Warningf("Found multiple SSH public keys - choosing arbitrarily") + } + sshPublicKey := sshPublicKeys[0] + l.Resources["ssh-public-key"] = fi.NewStringResource(string(sshPublicKey.Data)) } - l.TemplateFunctions["MachineTypeInfo"] = awsup.GetMachineTypeInfo } default: @@ -328,15 +338,6 @@ func (c *ApplyClusterCmd) Run() error { tf.AddTo(l.TemplateFunctions) - if c.SSHPublicKey != "" { - authorized, err := ioutil.ReadFile(c.SSHPublicKey) - if err != nil { - return fmt.Errorf("error reading SSH key file %q: %v", c.SSHPublicKey, err) - } - - l.Resources["ssh-public-key"] = fi.NewStringResource(string(authorized)) - } - taskMap, err := l.BuildTasks(modelStore, c.Models) if err != nil { return fmt.Errorf("error building tasks: %v", err) diff --git a/upup/pkg/fi/fitasks/secret.go b/upup/pkg/fi/fitasks/secret.go index a327bb5cbe..695b46a613 100644 --- a/upup/pkg/fi/fitasks/secret.go +++ b/upup/pkg/fi/fitasks/secret.go @@ -61,7 +61,12 @@ func (_ *Secret) Render(c *fi.Context, a, e, changes *Secret) error { secrets := c.SecretStore - _, _, err := secrets.GetOrCreateSecret(name) + secret, err := fi.CreateSecret() + if err != nil { + return fmt.Errorf("error creating secret %q: %v", name, err) + } + + _, _, err = secrets.GetOrCreateSecret(name, secret) if err != nil { return fmt.Errorf("error creating secret %q: %v", name, err) } diff --git a/upup/pkg/fi/secrets.go b/upup/pkg/fi/secrets.go index 8ac4cedba0..b5f94a844d 100644 --- a/upup/pkg/fi/secrets.go +++ b/upup/pkg/fi/secrets.go @@ -14,7 +14,7 @@ type SecretStore interface { // Find a secret, if exists. Returns nil,nil if not found FindSecret(id string) (*Secret, error) // Create or replace a secret - GetOrCreateSecret(id string) (secret *Secret, created bool, err error) + GetOrCreateSecret(id string, secret *Secret) (current *Secret, created bool, err error) // Lists the ids of all known secrets ListSecrets() ([]string, error) diff --git a/upup/pkg/fi/vfs_castore.go b/upup/pkg/fi/vfs_castore.go index fe4a5042c2..a0c84fb7ee 100644 --- a/upup/pkg/fi/vfs_castore.go +++ b/upup/pkg/fi/vfs_castore.go @@ -2,12 +2,14 @@ package fi import ( "bytes" + "crypto/md5" crypto_rand "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "fmt" "github.com/golang/glog" + "golang.org/x/crypto/ssh" "k8s.io/kops/upup/pkg/fi/vfs" "math/big" "os" @@ -25,6 +27,10 @@ type VFSCAStore struct { cacheCaPrivateKeys *privateKeys } +const ( + SecretTypeSSHPublicKey = "SSHPublicKey" +) + var _ CAStore = &VFSCAStore{} func NewVFSCAStore(basedir vfs.Path) CAStore { @@ -572,3 +578,93 @@ func buildSerial(timestamp int64) *big.Int { return serial } + +// AddSSHPublicKey stores an SSH public key +func (c *VFSCAStore) AddSSHPublicKey(name string, pubkey []byte) error { + var id string + { + sshPublicKey, _, _, _, err := ssh.ParseAuthorizedKey(pubkey) + if err != nil { + return fmt.Errorf("error parsing public key: %v", err) + } + + // compute fingerprint to serve as id + h := md5.New() + _, err = h.Write(sshPublicKey.Marshal()) + if err != nil { + return err + } + id = fmt.Sprintf("%x", h.Sum(nil)) + } + + p := c.buildSSHPublicKeyPath(name, id) + return c.storeData(pubkey, p) +} + +func (c *VFSCAStore) buildSSHPublicKeyPath(name string, id string) vfs.Path { + return c.basedir.Join("ssh", "public", name, id) +} + +func (c *VFSCAStore) storeData(data []byte, p vfs.Path) error { + return p.WriteFile(data) +} + +func (c *VFSCAStore) FindSSHPublicKeys(name string) ([]*KeystoreItem, error) { + p := c.basedir.Join("ssh", "public", name) + + items, err := c.loadPath(p) + if err != nil { + return nil, err + } + for _, item := range items { + item.Type = SecretTypeSSHPublicKey + item.Name = name + } + return items, nil +} + +func (c *VFSCAStore) loadPath(p vfs.Path) ([]*KeystoreItem, error) { + files, err := p.ReadDir() + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + + var keystoreItems []*KeystoreItem + + for _, f := range files { + data, err := f.ReadFile() + if err != nil { + if os.IsNotExist(err) { + glog.V(2).Infof("Ignoring not-found issue reading %q", f) + continue + } + return nil, fmt.Errorf("error loading keystore item %q: %v", f, err) + } + name := f.Base() + keystoreItem := &KeystoreItem{ + Id: name, + Data: data, + } + keystoreItems = append(keystoreItems, keystoreItem) + } + + return keystoreItems, nil +} + +func (c *VFSCAStore) loadData(p vfs.Path) (*PrivateKey, error) { + data, err := p.ReadFile() + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + k, err := ParsePEMPrivateKey(data) + if err != nil { + return nil, fmt.Errorf("error parsing private key from %q: %v", p, err) + } + return k, err +} diff --git a/upup/pkg/fi/vfs_secretstore.go b/upup/pkg/fi/vfs_secretstore.go index 8f2f94290d..82762c67cc 100644 --- a/upup/pkg/fi/vfs_secretstore.go +++ b/upup/pkg/fi/vfs_secretstore.go @@ -62,7 +62,7 @@ func (c *VFSSecretStore) Secret(id string) (*Secret, error) { return s, nil } -func (c *VFSSecretStore) GetOrCreateSecret(id string) (*Secret, bool, error) { +func (c *VFSSecretStore) GetOrCreateSecret(id string, secret *Secret) (*Secret, bool, error) { p := c.buildSecretPath(id) for i := 0; i < 2; i++ { @@ -75,12 +75,7 @@ func (c *VFSSecretStore) GetOrCreateSecret(id string) (*Secret, bool, error) { return s, false, nil } - s, err = CreateSecret() - if err != nil { - return nil, false, err - } - - err = c.createSecret(s, p) + err = c.createSecret(secret, p) if err != nil { if os.IsExist(err) && i == 0 { glog.Infof("Got already-exists error when writing secret; likely due to concurrent creation. Will retry")