diff --git a/cmd/controller/certificates.go b/cmd/controller/certificates.go index 22680503df0a..8a47305193ac 100644 --- a/cmd/controller/certificates.go +++ b/cmd/controller/certificates.go @@ -63,7 +63,7 @@ func (c *Certificates) Init(ctx context.Context) error { return fmt.Errorf("failed to read ca cert: %w", err) } c.CACert = string(cert) - kubeConfigAPIUrl := fmt.Sprintf("https://localhost:%d", c.ClusterSpec.API.Port) + kubeConfigAPIUrl := fmt.Sprintf("https://%s:%d", c.ClusterSpec.API.APIServerAddress(), c.ClusterSpec.API.Port) eg.Go(func() error { // Front proxy CA if err := c.CertManager.EnsureCA("front-proxy-ca", "kubernetes-front-proxy-ca"); err != nil { diff --git a/cmd/controller/controller.go b/cmd/controller/controller.go index 85cbd1850033..f56dcd3d3722 100644 --- a/cmd/controller/controller.go +++ b/cmd/controller/controller.go @@ -185,6 +185,7 @@ func (c *command) start(ctx context.Context) error { } logrus.Infof("using api address: %s", nodeConfig.Spec.API.Address) + logrus.Infof("using api bind-address: %s", nodeConfig.Spec.API.BindAddress) logrus.Infof("using listen port: %d", nodeConfig.Spec.API.Port) logrus.Infof("using sans: %s", nodeConfig.Spec.API.SANs) diff --git a/docs/configuration.md b/docs/configuration.md index f051e4fad03e..6a3aabe23884 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -55,6 +55,7 @@ spec: externalAddress: my-lb-address.example.com k0sApiPort: 9443 port: 6443 + bindAddress: 192.0.2.1 sans: - 192.168.68.104 controllerManager: {} @@ -124,9 +125,10 @@ spec: ### `spec.api` | Element | Description | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +|--------------------------| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `externalAddress` | The loadbalancer address (for k0s controllers running behind a loadbalancer). Configures all cluster components to connect to this address and also configures this address for use when joining new nodes to the cluster. | -| `address` | Local address on which to bind an API. Also serves as one of the addresses pushed on the k0s create service certificate on the API. Defaults to first non-local address found on the node. | +| `address` | Local address on which to bind an API. Also serves as one of the addresses pushed on the k0s create service certificate on the API. Defaults to first non-local address found on the node. | +| `bindAddress` | The IP address for the Kubernetes API server to to listen on. The associated interface(s) must be reachable by the rest of the cluster. Will be added as an additional subject alternative name to the API server's TLS certificate. If blank or an unspecified address (`0.0.0.0` or `::`), all interfaces and IP address families will be used. This is effectively the value for the API server's `--bind-address` CLI flag. | | `sans` | List of additional addresses to push to API servers serving the certificate. | | `extraArgs` | Map of key-values (strings) for any extra arguments to pass down to Kubernetes api-server process. | | `port`ยน | Custom port for kube-api server to listen on (default: 6443) | diff --git a/pkg/apis/k0s/v1beta1/api.go b/pkg/apis/k0s/v1beta1/api.go index c70f25bc1565..b1cd075854a2 100644 --- a/pkg/apis/k0s/v1beta1/api.go +++ b/pkg/apis/k0s/v1beta1/api.go @@ -35,6 +35,9 @@ type APISpec struct { // Local address on which to bind an API Address string `json:"address"` + // The IP address for the Kubernetes API server to listen on. + BindAddress string `json:"bindAddress,omitempty"` + // The loadbalancer address (for k0s controllers running behind a loadbalancer) ExternalAddress string `json:"externalAddress,omitempty"` // Map of key-values (strings) for any extra arguments to pass down to Kubernetes api-server process @@ -57,11 +60,12 @@ func DefaultAPISpec() *APISpec { addresses, _ := iface.AllAddresses() publicAddress, _ := iface.FirstPublicAddress() return &APISpec{ - Port: defaultKasPort, - K0sAPIPort: 9443, - SANs: addresses, - Address: publicAddress, - ExtraArgs: make(map[string]string), + Port: defaultKasPort, + K0sAPIPort: 9443, + BindAddress: "0.0.0.0", + SANs: addresses, + Address: publicAddress, + ExtraArgs: make(map[string]string), } } @@ -100,10 +104,21 @@ func (a *APISpec) getExternalURIForPort(port int) string { return fmt.Sprintf("https://%s:%d", addr, port) } +// APIServerAddress returns the address the API is listening on +func (a *APISpec) APIServerAddress() string { + if a.BindAddress == "" { + return "localhost" + } + return a.BindAddress +} + // Sans return the given SANS plus all local adresses and externalAddress if given func (a *APISpec) Sans() []string { sans, _ := iface.AllAddresses() sans = append(sans, a.Address) + if a.BindAddress != "" { + sans = append(sans, a.BindAddress) + } sans = append(sans, a.SANs...) if a.ExternalAddress != "" { sans = append(sans, a.ExternalAddress) @@ -139,5 +154,10 @@ func (a *APISpec) Validate() []error { if a.ExternalAddress != "" { validateIPAddressOrDNSName(field.NewPath("externalAddress"), a.ExternalAddress) } + + if a.BindAddress != "" && !govalidator.IsIP(a.BindAddress) { + errors = append(errors, field.Invalid(field.NewPath("bindAddress"), a.BindAddress, "invalid IP address")) + } + return errors } diff --git a/pkg/apis/k0s/v1beta1/api_test.go b/pkg/apis/k0s/v1beta1/api_test.go index d7551d14baaf..52a2a3585266 100644 --- a/pkg/apis/k0s/v1beta1/api_test.go +++ b/pkg/apis/k0s/v1beta1/api_test.go @@ -35,7 +35,8 @@ func (s *APISuite) TestValidation() { s.T().Run("accepts_ipv6_as_address", func(t *testing.T) { a := APISpec{ - Address: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + Address: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + BindAddress: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", } s.Nil(a.Validate()) @@ -44,7 +45,8 @@ func (s *APISuite) TestValidation() { s.T().Run("invalid_api_address", func(t *testing.T) { a := APISpec{ - Address: "something.that.is.not.valid//(())", + Address: "something.that.is.not.valid//(())", + BindAddress: "0.0.0.0", } errors := a.Validate() @@ -56,7 +58,8 @@ func (s *APISuite) TestValidation() { s.T().Run("invalid_sans_address", func(t *testing.T) { a := APISpec{ - Address: "1.2.3.4", + Address: "1.2.3.4", + BindAddress: "0.0.0.0", SANs: []string{ "something.that.is.not.valid//(())", }, @@ -68,6 +71,18 @@ func (s *APISuite) TestValidation() { s.ErrorContains(errors[0], `sans[0]: Invalid value: "something.that.is.not.valid//(())": invalid IP address / DNS name`) } }) + + s.T().Run("invalid_api_bind_address", func(t *testing.T) { + a := APISpec{ + Address: "1.2.3.4", + BindAddress: "somehting.that.is.not.valid//(())", + } + + errors := a.Validate() + s.NotNil(errors) + s.Len(errors, 1) + s.Contains(errors[0].Error(), "invalid IP address") + }) } func TestApiSuite(t *testing.T) { diff --git a/pkg/component/controller/apiserver.go b/pkg/component/controller/apiserver.go index 9d2824170106..7adbc3105add 100644 --- a/pkg/component/controller/apiserver.go +++ b/pkg/component/controller/apiserver.go @@ -125,6 +125,10 @@ func (a *APIServer) Start(_ context.Context) error { "enable-admission-plugins": "NodeRestriction", } + if a.ClusterConfig.Spec.API.BindAddress != "" { + args["bind-address"] = a.ClusterConfig.Spec.API.BindAddress + } + apiAudiences := []string{"https://kubernetes.default.svc"} if a.EnableKonnectivity { @@ -229,7 +233,7 @@ func (a *APIServer) Ready() error { TLSClientConfig: tlsConfig, } client := &http.Client{Transport: tr} - resp, err := client.Get(fmt.Sprintf("https://localhost:%d/readyz?verbose", a.ClusterConfig.Spec.API.Port)) + resp, err := client.Get(fmt.Sprintf("https://%s:%d/readyz?verbose", a.ClusterConfig.Spec.API.APIServerAddress(), a.ClusterConfig.Spec.API.Port)) if err != nil { return err } diff --git a/static/manifests/v1beta1/CustomResourceDefinition/k0s.k0sproject.io_clusterconfigs.yaml b/static/manifests/v1beta1/CustomResourceDefinition/k0s.k0sproject.io_clusterconfigs.yaml index ded8bd9df4f2..9b8ec4fd330e 100644 --- a/static/manifests/v1beta1/CustomResourceDefinition/k0s.k0sproject.io_clusterconfigs.yaml +++ b/static/manifests/v1beta1/CustomResourceDefinition/k0s.k0sproject.io_clusterconfigs.yaml @@ -40,6 +40,10 @@ spec: address: description: Local address on which to bind an API type: string + bindAddress: + description: The IP address for the Kubernetes API server to listen + on. + type: string externalAddress: description: The loadbalancer address (for k0s controllers running behind a loadbalancer)