diff --git a/docs/services-firewalls.md b/docs/services-firewalls.md new file mode 100644 index 0000000000..c97beb29d7 --- /dev/null +++ b/docs/services-firewalls.md @@ -0,0 +1,37 @@ +# Services and Firewalls + +Many cloud providers (e.g. Google Compute Engine) define firewalls that help keep prevent inadvertent +exposure to the internet. When exposing a service to the external world, you may need to open up +one or more ports in these firewalls to serve traffic. This document describes this process, as +well as any provider specific details that may be necessary. + + +### Google Compute Engine +Google Compute Engine firewalls are documented [elsewhere](https://cloud.google.com/compute/docs/networking#firewalls_1). + +You can add a firewall with the ```gcloud``` command line tool: + +``` +gcloud compute firewall-rules create my-rule --allow=tcp: +``` + +**Note** +There is one important security note when using firewalls on Google Compute Engine: + +Firewalls are defined per-vm, rather than per-ip address. This means that if you open a firewall for that service's ports, +anything that serves on that port on that VM's host IP address may potentially serve traffic. + +Note that this is not a problem for other Kubernetes services, as they listen on IP addresses that are different than the +host node's external IP address. + +Consider: + * You create a Service with an external load balancer (IP Address 1.2.3.4) and port 80 + * You open the firewall for port 80 for all nodes in your cluster, so that the external Service actually can deliver packets to your Service + * You start an nginx server, running on port 80 on the host virtual machine (IP Address 2.3.4.5). This nginx is **also** exposed to the internet on the VM's external IP address. + +Consequently, please be careful when opening firewalls in Google Compute Engine or Google Container Engine. You may accidentally be exposing other services to the wilds of the internet. + +### Other cloud providers +Coming soon. + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/services-firewalls.md?pixel)]() diff --git a/pkg/kubectl/cmd/create.go b/pkg/kubectl/cmd/create.go index 064cab8d9c..44f78bbb3b 100644 --- a/pkg/kubectl/cmd/create.go +++ b/pkg/kubectl/cmd/create.go @@ -19,12 +19,15 @@ package cmd import ( "fmt" "io" + "strings" "github.com/spf13/cobra" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) @@ -102,6 +105,7 @@ func RunCreate(f *cmdutil.Factory, out io.Writer, filenames util.StringList) err } count++ info.Refresh(obj, true) + printObjectSpecificMessage(info.Object, out) fmt.Fprintf(out, "%s/%s\n", info.Mapping.Resource, info.Name) return nil }) @@ -113,3 +117,36 @@ func RunCreate(f *cmdutil.Factory, out io.Writer, filenames util.StringList) err } return nil } + +func printObjectSpecificMessage(obj runtime.Object, out io.Writer) { + switch obj := obj.(type) { + case *api.Service: + if obj.Spec.Type == api.ServiceTypeLoadBalancer { + msg := fmt.Sprintf(` + An external load-balanced service was created. On many platforms (e.g. Google Compute Engine), + you will also need to explicitly open a Firewall rule for the service port(s) (%s) to serve traffic. + + See https://github.com/GoogleCloudPlatform/kubernetes/tree/master/docs/services-firewall.md for more details. + `, makePortsString(obj.Spec.Ports)) + out.Write([]byte(msg)) + } + if obj.Spec.Type == api.ServiceTypeNodePort { + msg := fmt.Sprintf(` + You have exposed your service on an external port on all nodes in your cluster. + If you want to expose this service to the external internet, you may need to set up + firewall rules for the service port(s) (%s) to serve traffic. + + See https://github.com/GoogleCloudPlatform/kubernetes/tree/master/docs/services-firewall.md for more details. + `, makePortsString(obj.Spec.Ports)) + out.Write([]byte(msg)) + } + } +} + +func makePortsString(ports []api.ServicePort) string { + pieces := make([]string, len(ports)) + for ix := range ports { + pieces[ix] = fmt.Sprintf("%s:%d", strings.ToLower(string(ports[ix].Protocol)), ports[ix].Port) + } + return strings.Join(pieces, ",") +} diff --git a/pkg/kubectl/cmd/create_test.go b/pkg/kubectl/cmd/create_test.go index 58dceea091..08a60baf93 100644 --- a/pkg/kubectl/cmd/create_test.go +++ b/pkg/kubectl/cmd/create_test.go @@ -21,7 +21,9 @@ import ( "net/http" "testing" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" ) func TestExtraArgsFail(t *testing.T) { @@ -129,3 +131,77 @@ func TestCreateDirectory(t *testing.T) { t.Errorf("unexpected output: %s", buf.String()) } } + +func TestPrintObjectSpecificMessage(t *testing.T) { + tests := []struct { + obj runtime.Object + expectOutput bool + }{ + { + obj: &api.Service{}, + expectOutput: false, + }, + { + obj: &api.Pod{}, + expectOutput: false, + }, + { + obj: &api.Service{Spec: api.ServiceSpec{Type: api.ServiceTypeLoadBalancer}}, + expectOutput: true, + }, + { + obj: &api.Service{Spec: api.ServiceSpec{Type: api.ServiceTypeNodePort}}, + expectOutput: true, + }, + } + for _, test := range tests { + buff := &bytes.Buffer{} + printObjectSpecificMessage(test.obj, buff) + if test.expectOutput && buff.Len() == 0 { + t.Errorf("Expected output, saw none for %v", test.obj) + } + if !test.expectOutput && buff.Len() > 0 { + t.Errorf("Expected no output, saw %s for %v", buff.String(), test.obj) + } + } +} + +func TestMakePortsString(t *testing.T) { + tests := []struct { + ports []api.ServicePort + expectedOutput string + }{ + {ports: nil, expectedOutput: ""}, + {ports: []api.ServicePort{}, expectedOutput: ""}, + {ports: []api.ServicePort{ + { + Port: 80, + Protocol: "TCP", + }, + }, + expectedOutput: "tcp:80", + }, + {ports: []api.ServicePort{ + { + Port: 80, + Protocol: "TCP", + }, + { + Port: 8080, + Protocol: "UDP", + }, + { + Port: 9000, + Protocol: "TCP", + }, + }, + expectedOutput: "tcp:80,udp:8080,tcp:9000", + }, + } + for _, test := range tests { + output := makePortsString(test.ports) + if output != test.expectedOutput { + t.Errorf("expected: %s, saw: %s.", test.expectedOutput, output) + } + } +} diff --git a/pkg/kubectl/cmd/expose.go b/pkg/kubectl/cmd/expose.go index f84cb3edc0..09ceb58777 100644 --- a/pkg/kubectl/cmd/expose.go +++ b/pkg/kubectl/cmd/expose.go @@ -201,5 +201,15 @@ func RunExpose(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []str } } + if cmdutil.GetFlagBool(cmd, "create-external-load-balancer") { + msg := fmt.Sprintf(` + An external load-balanced service was created. On many platforms (e.g. Google Compute Engine), + you will also need to explicitly open a firewall rule for the service port (%d) to serve traffic. + + See https://github.com/GoogleCloudPlatform/kubernetes/tree/master/docs/services-firewall.md for more details. + `, cmdutil.GetFlagInt(cmd, "port")) + out.Write([]byte(msg)) + } + return f.PrintObject(cmd, object, out) } diff --git a/pkg/kubectl/cmd/expose_test.go b/pkg/kubectl/cmd/expose_test.go index 09746fdfef..0609745e9c 100644 --- a/pkg/kubectl/cmd/expose_test.go +++ b/pkg/kubectl/cmd/expose_test.go @@ -69,8 +69,7 @@ func TestRunExposeService(t *testing.T) { Selector: map[string]string{"func": "stream"}, }, }, - expected: "services/foo", - status: 200, + status: 200, }, { name: "no-name-passed-from-the-cli", @@ -103,7 +102,40 @@ func TestRunExposeService(t *testing.T) { Selector: map[string]string{"run": "this"}, }, }, - expected: "services/mayor", + status: 200, + }, + { + name: "expose-external-service", + args: []string{"service", "baz"}, + ns: "test", + calls: map[string]string{ + "GET": "/namespaces/test/services/baz", + "POST": "/namespaces/test/services", + }, + input: &api.Service{ + ObjectMeta: api.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, + TypeMeta: api.TypeMeta{Kind: "Service", APIVersion: "v1"}, + Spec: api.ServiceSpec{ + Selector: map[string]string{"app": "go"}, + }, + }, + flags: map[string]string{"selector": "func=stream", "protocol": "UDP", "port": "14", "name": "foo", "labels": "svc=test", "create-external-load-balancer": "true"}, + output: &api.Service{ + ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "12", Labels: map[string]string{"svc": "test"}}, + TypeMeta: api.TypeMeta{Kind: "Service", APIVersion: "v1"}, + Spec: api.ServiceSpec{ + Ports: []api.ServicePort{ + { + Name: "default", + Protocol: api.Protocol("UDP"), + Port: 14, + }, + }, + Selector: map[string]string{"func": "stream"}, + Type: api.ServiceTypeLoadBalancer, + }, + }, + expected: "you will also need to explicitly open a firewall", status: 200, }, } @@ -135,9 +167,11 @@ func TestRunExposeService(t *testing.T) { } cmd.Run(cmd, test.args) - out := buf.String() - if strings.Contains(out, test.expected) { - t.Errorf("%s: unexpected output: %s", test.name, out) + if len(test.expected) > 0 { + out := buf.String() + if !strings.Contains(out, test.expected) { + t.Errorf("%s: unexpected output: %s", test.name, out) + } } } } diff --git a/pkg/kubectl/cmd/update.go b/pkg/kubectl/cmd/update.go index 52d98baa62..d0d63038b0 100644 --- a/pkg/kubectl/cmd/update.go +++ b/pkg/kubectl/cmd/update.go @@ -115,6 +115,7 @@ func RunUpdate(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []str return err } info.Refresh(obj, true) + printObjectSpecificMessage(obj, out) fmt.Fprintf(out, "%s/%s\n", info.Mapping.Resource, info.Name) return nil })