diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index dc4b4f6859b..111c6c6cb77 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -357,6 +357,11 @@ func (s *APIServer) Run(_ []string) error { } } + var installSSH master.InstallSSHKey + instances, supported := cloud.Instances() + if supported { + installSSH = instances.AddSSHKeyToAllInstances + } config := &master.Config{ EtcdHelper: helper, EventTTL: s.EventTTL, @@ -384,6 +389,7 @@ func (s *APIServer) Run(_ []string) error { MinRequestTimeout: s.MinRequestTimeout, SSHUser: s.SSHUser, SSHKeyfile: s.SSHKeyfile, + InstallSSHKey: installSSH, } m := master.New(config) diff --git a/pkg/cloudprovider/aws/aws.go b/pkg/cloudprovider/aws/aws.go index 6262f016a4e..70a77e8cc92 100644 --- a/pkg/cloudprovider/aws/aws.go +++ b/pkg/cloudprovider/aws/aws.go @@ -230,6 +230,10 @@ func newEc2Filter(name string, value string) *ec2.Filter { return filter } +func (self *AWSCloud) AddSSHKeyToAllInstances(user string, keyData []byte) error { + return errors.New("unimplemented") +} + // Implementation of EC2.Instances func (self *awsSdkEC2) DescribeInstances(request *ec2.DescribeInstancesInput) ([]*ec2.Instance, error) { // Instances are paged diff --git a/pkg/cloudprovider/cloud.go b/pkg/cloudprovider/cloud.go index 13fdaf4bf30..e5d371ffeb3 100644 --- a/pkg/cloudprovider/cloud.go +++ b/pkg/cloudprovider/cloud.go @@ -108,6 +108,9 @@ type Instances interface { List(filter string) ([]string, error) // GetNodeResources gets the resources for a particular node GetNodeResources(name string) (*api.NodeResources, error) + // AddSSHKeyToAllInstances adds an SSH public key as a legal identity for all instances + // expected format for the key is standard ssh-keygen format: + AddSSHKeyToAllInstances(user string, keyData []byte) error } // Route is a representation of an advanced routing rule. diff --git a/pkg/cloudprovider/fake/fake.go b/pkg/cloudprovider/fake/fake.go index 45ea2468b8a..b3a235f543d 100644 --- a/pkg/cloudprovider/fake/fake.go +++ b/pkg/cloudprovider/fake/fake.go @@ -144,6 +144,10 @@ func (f *FakeCloud) EnsureTCPLoadBalancerDeleted(name, region string) error { return f.Err } +func (f *FakeCloud) AddSSHKeyToAllInstances(user string, keyData []byte) error { + return errors.New("unimplemented") +} + // NodeAddresses is a test-spy implementation of Instances.NodeAddresses. // It adds an entry "node-addresses" into the internal method call record. func (f *FakeCloud) NodeAddresses(instance string) ([]api.NodeAddress, error) { diff --git a/pkg/cloudprovider/gce/gce.go b/pkg/cloudprovider/gce/gce.go index 50ac1fbcc2c..7e9ae02f24e 100644 --- a/pkg/cloudprovider/gce/gce.go +++ b/pkg/cloudprovider/gce/gce.go @@ -22,6 +22,7 @@ import ( "io/ioutil" "net" "net/http" + "os" "path" "strconv" "strings" @@ -474,6 +475,39 @@ func (gce *GCECloud) getInstanceByName(name string) (*compute.Instance, error) { return res, nil } +func (gce *GCECloud) AddSSHKeyToAllInstances(user string, keyData []byte) error { + project, err := gce.service.Projects.Get(gce.projectID).Do() + if err != nil { + return err + } + hostname, err := os.Hostname() + if err != nil { + return err + } + found := false + for _, item := range project.CommonInstanceMetadata.Items { + if item.Key == "sshKeys" { + item.Value = fmt.Sprintf("%s\n%s:%s %s@%s", item.Value, user, string(keyData), user, hostname) + found = true + break + } + } + if !found { + // This is super unlikely, so log. + glog.Infof("Failed to find sshKeys metadata, creating a new item") + project.CommonInstanceMetadata.Items = append(project.CommonInstanceMetadata.Items, + &compute.MetadataItems{ + Key: "sshKeys", + Value: fmt.Sprint("%s:%s %s@%s", user, string(keyData), user, hostname), + }) + } + op, err := gce.service.Projects.SetCommonInstanceMetadata(gce.projectID, project.CommonInstanceMetadata).Do() + if err != nil { + return err + } + return gce.waitForGlobalOp(op) +} + // NodeAddresses is an implementation of Instances.NodeAddresses. func (gce *GCECloud) NodeAddresses(_ string) ([]api.NodeAddress, error) { externalIP, err := gce.metadataAccess(EXTERNAL_IP_METADATA_URL) diff --git a/pkg/cloudprovider/mesos/mesos.go b/pkg/cloudprovider/mesos/mesos.go index cacc3b40b17..600c9050ac3 100644 --- a/pkg/cloudprovider/mesos/mesos.go +++ b/pkg/cloudprovider/mesos/mesos.go @@ -78,6 +78,10 @@ func newMesosCloud(configReader io.Reader) (*MesosCloud, error) { } } +func (c *MesosCloud) AddSSHKeyToAllInstances(user string, keyData []byte) error { + return errors.New("unimplemented") +} + // Instances returns a copy of the Mesos cloud Instances implementation. // Mesos natively provides minimal cloud-type resources. More robust cloud // support requires a combination of Mesos and cloud-specific knowledge. diff --git a/pkg/cloudprovider/openstack/openstack.go b/pkg/cloudprovider/openstack/openstack.go index 38520b28e7e..c4349ebf09d 100644 --- a/pkg/cloudprovider/openstack/openstack.go +++ b/pkg/cloudprovider/openstack/openstack.go @@ -317,6 +317,10 @@ func getAddressByName(api *gophercloud.ServiceClient, name string) (string, erro return s, nil } +func (i *Instances) AddSSHKeyToAllInstances(user string, keyData []byte) error { + return errors.New("unimplemented") +} + func (i *Instances) NodeAddresses(name string) ([]api.NodeAddress, error) { glog.V(4).Infof("NodeAddresses(%v) called", name) diff --git a/pkg/cloudprovider/ovirt/ovirt.go b/pkg/cloudprovider/ovirt/ovirt.go index 6c78202df1d..abcfff64539 100644 --- a/pkg/cloudprovider/ovirt/ovirt.go +++ b/pkg/cloudprovider/ovirt/ovirt.go @@ -18,6 +18,7 @@ package ovirt_cloud import ( "encoding/xml" + "errors" "fmt" "io" "io/ioutil" @@ -273,3 +274,7 @@ func (v *OVirtCloud) List(filter string) ([]string, error) { func (v *OVirtCloud) GetNodeResources(name string) (*api.NodeResources, error) { return nil, nil } + +func (v *OVirtCloud) AddSSHKeyToAllInstances(user string, keyData []byte) error { + return errors.New("unimplemented") +} diff --git a/pkg/cloudprovider/rackspace/rackspace.go b/pkg/cloudprovider/rackspace/rackspace.go index 8df8230da65..38f2c25ebaf 100644 --- a/pkg/cloudprovider/rackspace/rackspace.go +++ b/pkg/cloudprovider/rackspace/rackspace.go @@ -376,6 +376,10 @@ func (i *Instances) InstanceID(name string) (string, error) { return "", nil } +func (i *Instances) AddSSHKeyToAllInstances(user string, keyData []byte) error { + return errors.New("unimplemented") +} + func (i *Instances) GetNodeResources(name string) (*api.NodeResources, error) { glog.V(2).Infof("GetNodeResources(%v) called", name) diff --git a/pkg/cloudprovider/vagrant/vagrant.go b/pkg/cloudprovider/vagrant/vagrant.go index 2bd18150d17..e4e89ed453e 100644 --- a/pkg/cloudprovider/vagrant/vagrant.go +++ b/pkg/cloudprovider/vagrant/vagrant.go @@ -131,6 +131,10 @@ func (v *VagrantCloud) getInstanceByAddress(address string) (*SaltMinion, error) return nil, fmt.Errorf("unable to find instance for address: %s", address) } +func (v *VagrantCloud) AddSSHKeyToAllInstances(user string, keyData []byte) error { + return errors.New("unimplemented") +} + // NodeAddresses returns the NodeAddresses of a particular machine instance. func (v *VagrantCloud) NodeAddresses(instance string) ([]api.NodeAddress, error) { // Due to vagrant not running with a dedicated DNS setup, we return the IP address of a minion as its hostname at this time diff --git a/pkg/master/master.go b/pkg/master/master.go index 94835933052..a36b620634a 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -18,7 +18,9 @@ package master import ( "bytes" + "errors" "fmt" + "io/ioutil" "net" "net/http" "net/http/pprof" @@ -147,10 +149,13 @@ type Config struct { ServiceNodePortRange util.PortRange // Used for secure proxy. If empty, don't use secure proxy. - SSHUser string - SSHKeyfile string + SSHUser string + SSHKeyfile string + InstallSSHKey InstallSSHKey } +type InstallSSHKey func(user string, data []byte) error + // Master contains state for a Kubernetes cluster master/api server. type Master struct { // "Inputs", Copied from Config @@ -204,7 +209,8 @@ type Master struct { InsecureHandler http.Handler // Used for secure proxy - tunnels util.SSHTunnelList + tunnels util.SSHTunnelList + installSSHKey InstallSSHKey } // NewEtcdHelper returns an EtcdHelper for the provided arguments or an error if the version @@ -486,6 +492,16 @@ func (m *Master) init(c *Config) { var proxyDialer func(net, addr string) (net.Conn, error) if len(c.SSHUser) > 0 { glog.Infof("Setting up proxy: %s %s", c.SSHUser, c.SSHKeyfile) + exists, err := util.FileExists(c.SSHKeyfile) + if err != nil { + glog.Errorf("Error detecting if key exists: %v", err) + } else if !exists { + glog.Infof("Key doesn't exist, attempting to create") + err := m.generateSSHKey(c.SSHUser, c.SSHKeyfile) + if err != nil { + glog.Errorf("Failed to create key pair: %v", err) + } + } m.setupSecureProxy(c.SSHUser, c.SSHKeyfile) proxyDialer = m.Dial } @@ -801,3 +817,21 @@ func (m *Master) setupSecureProxy(user, keyfile string) { } }() } + +func (m *Master) generateSSHKey(user, keyfile string) error { + if m.installSSHKey == nil { + return errors.New("ssh install function is null") + } + + private, public, err := util.GenerateKey(2048) + if err != nil { + return err + } + ioutil.WriteFile(keyfile, util.EncodePrivateKey(private), 0600) + data, err := util.EncodeSSHKey(public) + if err != nil { + return err + } + fmt.Printf("FOO: %s", data) + return m.installSSHKey(user, data) +} diff --git a/pkg/util/ssh.go b/pkg/util/ssh.go index 09b82c2d200..2ed99e74bc6 100644 --- a/pkg/util/ssh.go +++ b/pkg/util/ssh.go @@ -18,10 +18,14 @@ package util import ( "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" "fmt" "io" "io/ioutil" - "math/rand" + mathrand "math/rand" "net" "os" @@ -260,7 +264,7 @@ func (l SSHTunnelList) Dial(network, addr string) (net.Conn, error) { if len(l) == 0 { return nil, fmt.Errorf("Empty tunnel list.") } - return l[rand.Int()%len(l)].Tunnel.Dial(network, addr) + return l[mathrand.Int()%len(l)].Tunnel.Dial(network, addr) } func (l SSHTunnelList) Has(addr string) bool { @@ -271,3 +275,38 @@ func (l SSHTunnelList) Has(addr string) bool { } return false } + +func EncodePrivateKey(private *rsa.PrivateKey) []byte { + return pem.EncodeToMemory(&pem.Block{ + Bytes: x509.MarshalPKCS1PrivateKey(private), + Type: "RSA PRIVATE KEY", + }) +} + +func EncodePublicKey(public *rsa.PublicKey) ([]byte, error) { + publicBytes, err := x509.MarshalPKIXPublicKey(public) + if err != nil { + return nil, err + } + + return pem.EncodeToMemory(&pem.Block{ + Bytes: publicBytes, + Type: "PUBLIC KEY", + }), nil +} + +func EncodeSSHKey(public *rsa.PublicKey) ([]byte, error) { + publicKey, err := ssh.NewPublicKey(public) + if err != nil { + return nil, err + } + return ssh.MarshalAuthorizedKey(publicKey), nil +} + +func GenerateKey(bits int) (*rsa.PrivateKey, *rsa.PublicKey, error) { + private, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, nil, err + } + return private, &private.PublicKey, nil +} diff --git a/pkg/util/util.go b/pkg/util/util.go index 7ba9c78f3dd..a6e33774caf 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -515,4 +515,16 @@ func ShortenString(str string, n int) string { } else { return str[:n] } + +func FileExists(filename string) (bool, error) { + file, err := os.Open(filename) + defer file.Close() + if err != nil { + if os.IsNotExist(err) { + return false, nil + } else { + return false, err + } + } + return true, nil }