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 certificate based authentication to localkube/minikube. #48

Merged
merged 10 commits into from
May 9, 2016
15 changes: 2 additions & 13 deletions cmd/minikube/cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,13 @@ limitations under the License.
package cmd

import (
"io/ioutil"
"log"
"os"
"testing"

"github.com/spf13/cobra"
"k8s.io/minikube/pkg/minikube/constants"
"k8s.io/minikube/pkg/minikube/tests"
)

func makeTempDir() string {
tempDir, err := ioutil.TempDir("", "minipath")
if err != nil {
log.Fatal(err)
}
constants.Minipath = tempDir
return tempDir
}

func runCommand(f func(*cobra.Command, []string)) {
cmd := cobra.Command{}
var args []string
Expand All @@ -43,7 +32,7 @@ func runCommand(f func(*cobra.Command, []string)) {

func TestPreRunDirectories(t *testing.T) {
// Make sure we create the required directories.
tempDir := makeTempDir()
tempDir := tests.MakeTempDir()
defer os.RemoveAll(tempDir)

runCommand(RootCmd.PersistentPreRun)
Expand Down
26 changes: 22 additions & 4 deletions cmd/minikube/cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ assumes you already have Virtualbox installed.`,
Run: runStart,
}

var (
localkubeURL string
)

func runStart(cmd *cobra.Command, args []string) {

fmt.Println("Starting local Kubernetes cluster...")
Expand All @@ -48,7 +52,11 @@ func runStart(cmd *cobra.Command, args []string) {
os.Exit(1)
}

if err := cluster.StartCluster(host); err != nil {
config := cluster.KubernetesConfig{
LocalkubeURL: localkubeURL,
}

if err := cluster.StartCluster(host, config); err != nil {
log.Println("Error starting cluster: ", err)
os.Exit(1)
}
Expand All @@ -57,13 +65,23 @@ func runStart(cmd *cobra.Command, args []string) {
if err != nil {
log.Println("Error connecting to cluster: ", err)
}
kubeHost = strings.Replace(kubeHost, "tcp://", "http://", -1)
kubeHost = strings.Replace(kubeHost, ":2376", ":8080", -1)
kubeHost = strings.Replace(kubeHost, "tcp://", "https://", -1)
kubeHost = strings.Replace(kubeHost, ":2376", ":443", -1)
log.Printf("Kubernetes is available at %s.\n", kubeHost)
log.Println("Run this command to use the cluster: ")
log.Printf("kubectl config set-cluster minikube --insecure-skip-tls-verify=true --server=%s\n", kubeHost)
log.Printf("kubectl config set-cluster minikube --server=%s --certificate-authority=$HOME/.minikube/ca.crt\n", kubeHost)
log.Println("kubectl config set-credentials minikube --client-certificate=$HOME/.minikube/kubecfg.crt --client-key=$HOME/.minikube/kubecfg.key")
log.Println("kubectl config set-context minikube --cluster=minikube --user=minikube")
log.Println("kubectl config use-context minikube")

if err := cluster.GetCreds(host); err != nil {
log.Println("Error configuring authentication: ", err)
os.Exit(1)
}
}

func init() {
startCmd.Flags().StringVarP(&localkubeURL, "localkube-url", "", "https://storage.googleapis.com/tinykube/localkube", "Location of the localkube binary")
startCmd.Flags().MarkHidden("localkube-url")
RootCmd.AddCommand(startCmd)
}
17 changes: 14 additions & 3 deletions pkg/localkube/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"net"
"os"
"path/filepath"
"strings"
"time"

Expand All @@ -29,9 +30,12 @@ import (
)

const (
APIServerName = "apiserver"
APIServerHost = "0.0.0.0"
APIServerPort = 8080
APIServerName = "apiserver"
APIServerHost = "127.0.0.1"
APIServerPort = 8080
APIServerSecureHost = "0.0.0.0"
APIServerSecurePort = 443
certPath = "/srv/kubernetes/certs/"
)

var (
Expand Down Expand Up @@ -62,9 +66,16 @@ func StartAPIServer() {
config := options.NewAPIServer()

// use host/port from vars
config.BindAddress = net.ParseIP(APIServerSecureHost)
config.SecurePort = APIServerSecurePort
config.InsecureBindAddress = net.ParseIP(APIServerHost)
config.InsecurePort = APIServerPort

config.ClientCAFile = filepath.Join(certPath, "ca.crt")
config.TLSCertFile = filepath.Join(certPath, "kubernetes-master.crt")
config.TLSPrivateKeyFile = filepath.Join(certPath, "kubernetes-master.key")
config.AdmissionControl = "NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota"

// use localkube etcd
config.EtcdConfig = etcdstorage.EtcdConfig{
ServerList: KubeEtcdClientURLs,
Expand Down
2 changes: 2 additions & 0 deletions pkg/localkube/controller-manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package localkube

import (
"os"
"path/filepath"
"time"

controllerManager "k8s.io/kubernetes/cmd/kube-controller-manager/app"
Expand Down Expand Up @@ -50,6 +51,7 @@ func StartControllerManagerServer() {
config.DeletingPodsQps = 0.1
config.DeletingPodsBurst = 10
config.EnableProfiling = true
config.ServiceAccountKeyFile = filepath.Join(certPath, "kubernetes-master.key")

fn := func() error {
return controllerManager.Run(config)
Expand Down
5 changes: 1 addition & 4 deletions pkg/localkube/kubelet.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ const (
)

var (
WeaveProxySock = "unix:///var/run/weave/weave.sock"
KubeletStop chan struct{}
KubeletStop chan struct{}
)

func NewKubeletServer(clusterDomain, clusterDNS string, containerized bool) Server {
Expand All @@ -50,9 +49,7 @@ func StartKubeletServer(clusterDomain, clusterDNS string, containerized bool) fu
// master details
config.APIServerList = []string{APIServerURL}

// Docker
config.Containerized = containerized
config.DockerEndpoint = WeaveProxySock

// Networking
config.ClusterDomain = clusterDomain
Expand Down
62 changes: 37 additions & 25 deletions pkg/minikube/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ package cluster
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"path/filepath"
"strings"
"time"

Expand All @@ -30,6 +32,14 @@ import (
"k8s.io/minikube/pkg/minikube/constants"
)

const (
remotePath = "/srv/kubernetes/certs"
)

var (
certs = []string{"ca.crt", "kubecfg.key", "kubecfg.crt"}
)

// StartHost starts a host VM.
func StartHost(api libmachine.API) (*host.Host, error) {
if exists, err := api.Exists(constants.MachineName); err != nil {
Expand Down Expand Up @@ -128,35 +138,37 @@ type sshAble interface {
RunSSHCommand(string) (string, error)
}

// StartCluster starts as k8s cluster on the specified Host.
func StartCluster(h sshAble) error {
for _, cmd := range []string{
// Download and install weave, if it doesn't exist.
`if [ ! -e /usr/local/bin/weave ]; then
sudo curl -L git.io/weave -o /usr/local/bin/weave
sudo chmod a+x /usr/local/bin/weave;
fi`,
// Download and install localkube, if it doesn't exist yet.
`if [ ! -e /usr/local/bin/localkube ];
then
sudo curl -L https://github.com/redspread/localkube/releases/download/v1.2.1-v1/localkube-linux -o /usr/local/bin/localkube
sudo chmod a+x /usr/local/bin/localkube;
fi`,
// Start weave.
"weave launch-router",
"weave launch-proxy --without-dns --rewrite-inspect",
"weave expose -h \"localkube.weave.local\"",
// Localkube assumes containerized kubelet, which looks at /rootfs.
"if [ ! -e /rootfs ]; then sudo ln -s / /rootfs; fi",
// Run with nohup so it stays up. Redirect logs to useful places.
"PATH=/usr/local/sbin:$PATH nohup sudo /usr/local/bin/localkube start > /var/log/localkube.out 2> /var/log/localkube.err < /dev/null &"} {
output, err := h.RunSSHCommand(cmd)
log.Println(output)
// KubernetesConfig contains the parameters used to start a cluster.
type KubernetesConfig struct {
LocalkubeURL string
}

// StartCluster starts a k8s cluster on the specified Host.
func StartCluster(h sshAble, config KubernetesConfig) error {
output, err := h.RunSSHCommand(fmt.Sprintf(startCommand, config.LocalkubeURL))
log.Println(output)
if err != nil {
return err
}

return nil
}

// GetCreds gets the generated credentials required to talk to the APIServer.
func GetCreds(h sshAble) error {
localPath := constants.Minipath

for _, cert := range certs {
remoteCertPath := filepath.Join(remotePath, cert)
localCertPath := filepath.Join(localPath, cert)
data, err := h.RunSSHCommand(fmt.Sprintf("cat %s", remoteCertPath))
if err != nil {
return err
}
if err := ioutil.WriteFile(localCertPath, []byte(data), 0644); err != nil {
return err
}
}

return nil
}

Expand Down
80 changes: 69 additions & 11 deletions pkg/minikube/cluster/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ package cluster

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strings"
"testing"

Expand Down Expand Up @@ -57,32 +61,36 @@ func TestCreateHost(t *testing.T) {
}
}

// Mock Host used for testing. When commands are run, the output from CommandOutput
// is used, if present. Then the output from Error is used, if present. Finally,
// "", nil is returned.
type mockHost struct {
Commands []string
CommandOutput map[string]string
Error string
}

func (m mockHost) RunSSHCommand(cmd string) (string, error) {
m.Commands = append(m.Commands, cmd)
output, ok := m.CommandOutput[cmd]
if ok {
return output, nil
}
if m.Error != "" {
return "", fmt.Errorf(m.Error)
}
return "", nil
}

func TestStartCluster(t *testing.T) {
h := mockHost{}
err := StartCluster(h)
err := StartCluster(h, KubernetesConfig{})
if err != nil {
t.Fatalf("Error starting cluster: %s", err)
}
}

type mockHostError struct{}

func (m mockHostError) RunSSHCommand(cmd string) (string, error) {
return "", fmt.Errorf("Error calling command: %s", cmd)
}

func TestStartClusterError(t *testing.T) {
h := mockHostError{}
err := StartCluster(h)
h := mockHost{Error: "error"}
err := StartCluster(h, KubernetesConfig{})
if err == nil {
t.Fatal("Error not thrown starting cluster.")
}
Expand Down Expand Up @@ -271,3 +279,53 @@ func TestGetHostStatus(t *testing.T) {
StopHost(api)
checkState(state.Stopped.String())
}

func TestGetCreds(t *testing.T) {
m := make(map[string]string)
for _, cert := range certs {
m[fmt.Sprintf("cat %s/%s", remotePath, cert)] = cert
}

h := mockHost{CommandOutput: m}

tempDir := tests.MakeTempDir()
defer os.RemoveAll(tempDir)

if err := GetCreds(h); err != nil {
t.Fatalf("Error starting cluster: %s", err)
}

for _, cert := range certs {
// Files should be created with contents matching the output.
certPath := filepath.Join(tempDir, cert)
contents, err := ioutil.ReadFile(certPath)
if err != nil {
t.Fatalf("Error %s reading file: %s", err, certPath)
}
if !reflect.DeepEqual(contents, []byte(cert)) {
t.Fatalf("Contents of file are: %s, should be %s", contents, cert)
}
}
}

func TestGetCredsError(t *testing.T) {
h := mockHost{
Error: "error getting creds",
}
tempDir := tests.MakeTempDir()
defer os.RemoveAll(tempDir)

if err := GetCreds(h); err == nil {
t.Fatalf("Error should have been thrown, but was not.")
}

// No files should have been created.
for _, cert := range certs {
certPath := filepath.Join(tempDir, cert)
_, err := os.Stat(certPath)
if !os.IsNotExist(err) {
t.Fatalf("File %s should not exist.", certPath)
}
}

}
34 changes: 34 additions & 0 deletions pkg/minikube/cluster/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package cluster

var startCommand = `
sudo killall localkube || true
# Download and install localkube, if it doesn't exist yet.
if [ ! -e /usr/local/bin/localkube ]; then
sudo curl -L %s -o /usr/local/bin/localkube
sudo chmod a+x /usr/local/bin/localkube;
fi
# Fetch easy-rsa.
sudo mkdir -p /srv/kubernetes/certs && sudo chmod -R 777 /srv
if [ ! -e easy-rsa.tar.gz ]; then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about creating the certs with go instead?
Maybe that's easier...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried a few different ways, using https://github.com/SvenDowideit/generate_cert/blob/master/generate_cert.go but was unable to get anything working in pure go :(

curl -L -O https://storage.googleapis.com/kubernetes-release/easy-rsa/easy-rsa.tar.gz
fi
rm -rf easy-rsa-master
tar xzf easy-rsa.tar.gz
# Create certs.
cert_ip=$(ip addr show ${interface} | grep 192.168 | sed -nEe 's/^[ \t]*inet[ \t]*([0-9.]+)\/.*$/\1/p')
ts=$(date +%%s)
if ! grep $cert_ip /srv/kubernetes/certs/kubernetes-master.crt; then
cd easy-rsa-master/easyrsa3
./easyrsa init-pki
./easyrsa --batch "--req-cn=$cert_ip@$ts" build-ca nopass
./easyrsa --subject-alt-name="IP:$cert_ip" build-server-full kubernetes-master nopass
./easyrsa build-client-full kubecfg nopass
cp -p pki/ca.crt /srv/kubernetes/certs/
cp -p pki/issued/kubecfg.crt /srv/kubernetes/certs/
cp -p pki/private/kubecfg.key /srv/kubernetes/certs/
cp -p pki/issued/kubernetes-master.crt /srv/kubernetes/certs/
cp -p pki/private/kubernetes-master.key /srv/kubernetes/certs/
fi
# Run with nohup so it stays up. Redirect logs to useful places.
PATH=/usr/local/sbin:$PATH nohup sudo /usr/local/bin/localkube --containerized=false start > /var/log/localkube.out 2> /var/log/localkube.err < /dev/null &
`
Loading