Skip to content

add tls configuration #2189

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 58 additions & 15 deletions cmd/installer/cli/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
Expand All @@ -27,6 +28,7 @@ import (
ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1"
"github.com/replicatedhq/embedded-cluster/kinds/types"
newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config"
"github.com/replicatedhq/embedded-cluster/pkg-new/tlsutils"
"github.com/replicatedhq/embedded-cluster/pkg/addons"
"github.com/replicatedhq/embedded-cluster/pkg/addons/adminconsole"
"github.com/replicatedhq/embedded-cluster/pkg/addons/embeddedclusteroperator"
Expand Down Expand Up @@ -85,6 +87,9 @@ type InstallCmdFlags struct {
// guided UI flags
managerPort int
guidedUI bool
certFile string
keyFile string
hostname string
}

// InstallCmd returns a cobra command for installing the embedded cluster.
Expand Down Expand Up @@ -207,13 +212,25 @@ func addInstallAdminConsoleFlags(cmd *cobra.Command, flags *InstallCmdFlags) err
func addGuidedUIFlags(cmd *cobra.Command, flags *InstallCmdFlags) error {
cmd.Flags().BoolVarP(&flags.guidedUI, "guided-ui", "g", false, "Run the installation in guided UI mode.")
cmd.Flags().IntVar(&flags.managerPort, "manager-port", ecv1beta1.DefaultManagerPort, "Port on which the Manager will be served")
cmd.Flags().StringVar(&flags.certFile, "cert-file", "", "Path to the TLS certificate file")
cmd.Flags().StringVar(&flags.keyFile, "key-file", "", "Path to the TLS key file")
cmd.Flags().StringVar(&flags.hostname, "hostname", "", "Hostname to use for TLS configuration")

if err := cmd.Flags().MarkHidden("guided-ui"); err != nil {
return err
}
if err := cmd.Flags().MarkHidden("manager-port"); err != nil {
return err
}
if err := cmd.Flags().MarkHidden("cert-file"); err != nil {
return err
}
if err := cmd.Flags().MarkHidden("key-file"); err != nil {
return err
}
if err := cmd.Flags().MarkHidden("hostname"); err != nil {
return err
}

return nil
}
Expand Down Expand Up @@ -261,13 +278,30 @@ func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags) error {
configChan := make(chan *apitypes.InstallationConfig)
defer close(configChan)

if err := preRunInstallAPI(cmd.Context(), flags.adminConsolePassword, flags.managerPort, configChan); err != nil {
// this is necessary because the api listens on all interfaces,
// and we only know the interface to use when the user selects it in the ui
ipAddresses, err := netutils.ListAllValidIPAddresses()
if err != nil {
return fmt.Errorf("unable to list all valid IP addresses: %w", err)
}

cert, err := tlsutils.GetCertificate(tlsutils.Config{
CertFile: flags.certFile,
KeyFile: flags.keyFile,
Hostname: flags.hostname,
IPAddresses: ipAddresses,
})
if err != nil {
return fmt.Errorf("get tls certificate: %w", err)
}

if err := preRunInstallAPI(cmd.Context(), cert, flags.adminConsolePassword, flags.managerPort, configChan); err != nil {
return fmt.Errorf("unable to start install API: %w", err)
}

// TODO: fix this message
logrus.Info("")
logrus.Infof("Visit %s to configure your cluster", getManagerURL(flags.managerPort))
logrus.Infof("Visit %s to configure your cluster", getManagerURL(flags.hostname, flags.managerPort))

installConfig, ok := <-configChan
if !ok {
Expand Down Expand Up @@ -363,7 +397,7 @@ func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags) error {
return nil
}

func preRunInstallAPI(ctx context.Context, password string, managerPort int, configChan chan<- *apitypes.InstallationConfig) error {
func preRunInstallAPI(ctx context.Context, cert tls.Certificate, password string, managerPort int, configChan chan<- *apitypes.InstallationConfig) error {
logger, err := api.NewLogger()
if err != nil {
logrus.Warnf("Unable to setup API logging: %v", err)
Expand All @@ -375,7 +409,7 @@ func preRunInstallAPI(ctx context.Context, password string, managerPort int, con
}

go func() {
if err := runInstallAPI(ctx, listener, logger, password, configChan); err != nil {
if err := runInstallAPI(ctx, listener, cert, logger, password, configChan); err != nil {
if !errors.Is(err, http.ErrServerClosed) {
logrus.Errorf("install API error: %v", err)
}
Expand All @@ -389,7 +423,7 @@ func preRunInstallAPI(ctx context.Context, password string, managerPort int, con
return nil
}

func runInstallAPI(ctx context.Context, listener net.Listener, logger logrus.FieldLogger, password string, configChan chan<- *apitypes.InstallationConfig) error {
func runInstallAPI(ctx context.Context, listener net.Listener, cert tls.Certificate, logger logrus.FieldLogger, password string, configChan chan<- *apitypes.InstallationConfig) error {
router := mux.NewRouter()

api, err := api.New(
Expand All @@ -412,7 +446,8 @@ func runInstallAPI(ctx context.Context, listener net.Listener, logger logrus.Fie
router.PathPrefix("/").Methods("GET").Handler(webFs)

server := &http.Server{
Handler: router,
Handler: router,
TLSConfig: tlsutils.GetTLSConfig(cert),
}

go func() {
Expand All @@ -421,15 +456,17 @@ func runInstallAPI(ctx context.Context, listener net.Listener, logger logrus.Fie
server.Shutdown(context.Background())
}()

logrus.Debugf("Install API listening on %s", listener.Addr().String())
return server.Serve(listener)
return server.ServeTLS(listener, "", "")
}

func waitForInstallAPI(ctx context.Context, addr string) error {
httpClient := http.Client{
Timeout: 2 * time.Second,
Transport: &http.Transport{
Proxy: nil,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
timeout := time.After(10 * time.Second)
Expand All @@ -442,7 +479,7 @@ func waitForInstallAPI(ctx context.Context, addr string) error {
}
return fmt.Errorf("install API did not start in time")
case <-time.Tick(1 * time.Second):
resp, err := httpClient.Get(fmt.Sprintf("http://%s/api/health", addr))
resp, err := httpClient.Get(fmt.Sprintf("https://%s/api/health", addr))
if err != nil {
lastErr = fmt.Errorf("unable to connect to install API: %w", err)
} else if resp.StatusCode == http.StatusOK {
Expand Down Expand Up @@ -578,7 +615,7 @@ func runInstall(ctx context.Context, name string, flags InstallCmdFlags, metrics
return fmt.Errorf("unable to mark ui install complete: %w", err)
}
} else {
if err := printSuccessMessage(flags.license, flags.networkInterface); err != nil {
if err := printSuccessMessage(flags.license, flags.hostname, flags.networkInterface); err != nil {
return err
}
}
Expand Down Expand Up @@ -1484,8 +1521,8 @@ func copyLicenseFileToDataDir(licenseFile, dataDir string) error {
return nil
}

func printSuccessMessage(license *kotsv1beta1.License, networkInterface string) error {
adminConsoleURL := getAdminConsoleURL(networkInterface, runtimeconfig.AdminConsolePort())
func printSuccessMessage(license *kotsv1beta1.License, hostname string, networkInterface string) error {
adminConsoleURL := getAdminConsoleURL(hostname, networkInterface, runtimeconfig.AdminConsolePort())

// Create the message content
message := fmt.Sprintf("Visit the Admin Console to configure and install %s:", license.Spec.AppSlug)
Expand Down Expand Up @@ -1515,7 +1552,10 @@ func printSuccessMessage(license *kotsv1beta1.License, networkInterface string)
return nil
}

func getManagerURL(port int) string {
func getManagerURL(hostname string, port int) string {
if hostname != "" {
return fmt.Sprintf("https://%s:%v", hostname, port)
}
ipaddr := runtimeconfig.TryDiscoverPublicIP()
if ipaddr == "" {
if addr := os.Getenv("EC_PUBLIC_ADDRESS"); addr != "" {
Expand All @@ -1525,10 +1565,13 @@ func getManagerURL(port int) string {
ipaddr = "NODE-IP-ADDRESS"
}
}
return fmt.Sprintf("http://%s:%v", ipaddr, port)
return fmt.Sprintf("https://%s:%v", ipaddr, port)
}

func getAdminConsoleURL(networkInterface string, port int) string {
func getAdminConsoleURL(hostname string, networkInterface string, port int) string {
if hostname != "" {
return fmt.Sprintf("http://%s:%v", hostname, port)
}
ipaddr := runtimeconfig.TryDiscoverPublicIP()
if ipaddr == "" {
var err error
Expand Down
2 changes: 1 addition & 1 deletion cmd/installer/cli/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -1579,7 +1579,7 @@ func waitForAdditionalNodes(ctx context.Context, highAvailability bool, networkI
return fmt.Errorf("unable to create kube client: %w", err)
}

adminConsoleURL := getAdminConsoleURL(networkInterface, runtimeconfig.AdminConsolePort())
adminConsoleURL := getAdminConsoleURL("", networkInterface, runtimeconfig.AdminConsolePort())

successColor := "\033[32m"
colorReset := "\033[0m"
Expand Down
83 changes: 83 additions & 0 deletions pkg-new/tlsutils/tls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package tlsutils

import (
"crypto/tls"
"fmt"
"net"

"github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig"
"github.com/sirupsen/logrus"
certutil "k8s.io/client-go/util/cert"
)

var (
// TLSCipherSuites defines the allowed cipher suites for TLS connections
TLSCipherSuites = []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
}
)

// Config represents TLS configuration options
type Config struct {
CertFile string
KeyFile string
Hostname string
IPAddresses []net.IP
}

// GetCertificate returns a TLS certificate based on the provided configuration.
// If cert and key files are provided, it uses those. Otherwise, it generates a self-signed certificate.
func GetCertificate(cfg Config) (tls.Certificate, error) {
if cfg.CertFile != "" && cfg.KeyFile != "" {
logrus.Debugf("Using TLS configuration with cert file: %s and key file: %s", cfg.CertFile, cfg.KeyFile)
return tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile)
}

hostname, altNames := generateCertHostnames(cfg.Hostname)

// Generate a new self-signed cert
certData, keyData, err := certutil.GenerateSelfSignedCertKey(hostname, cfg.IPAddresses, altNames)
if err != nil {
return tls.Certificate{}, fmt.Errorf("generate self-signed cert: %w", err)
}

cert, err := tls.X509KeyPair(certData, keyData)
if err != nil {
return tls.Certificate{}, fmt.Errorf("create TLS certificate: %w", err)
}

logrus.Debugf("Using self-signed TLS certificate for hostname: %s", hostname)
return cert, nil
}

// GetTLSConfig returns a TLS configuration with the provided certificate
func GetTLSConfig(cert tls.Certificate) *tls.Config {
return &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: TLSCipherSuites,
Certificates: []tls.Certificate{cert},
}
}

func generateCertHostnames(hostname string) (string, []string) {
namespace := runtimeconfig.KotsadmNamespace

if hostname == "" {
hostname = fmt.Sprintf("kotsadm.%s.svc.cluster.local", namespace)
}

altNames := []string{
"kotsadm",
fmt.Sprintf("kotsadm.%s", namespace),
fmt.Sprintf("kotsadm.%s.svc", namespace),
fmt.Sprintf("kotsadm.%s.svc.cluster", namespace),
fmt.Sprintf("kotsadm.%s.svc.cluster.local", namespace),
}

return hostname, altNames
}
31 changes: 31 additions & 0 deletions pkg/netutils/ips.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"net"
"strings"

"github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig"
)

// adapted from https://github.com/k0sproject/k0s/blob/v1.30.4%2Bk0s.0/internal/pkg/iface/iface.go#L61
Expand Down Expand Up @@ -114,3 +116,32 @@ func firstValidIPNet(i net.Interface) (*net.IPNet, error) {
}
return nil, fmt.Errorf("could not find any non-local, non podnetwork ipv4 addresses")
}

func ListAllValidIPAddresses() ([]net.IP, error) {
ipAddresses := []net.IP{}

ifs, err := ListValidNetworkInterfaces()
if err != nil {
return nil, fmt.Errorf("list valid network interfaces: %w", err)
}
for _, i := range ifs {
addrs, err := i.Addrs()
if err != nil {
return nil, fmt.Errorf("get addresses: %w", err)
}
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
ipAddresses = append(ipAddresses, ipnet.IP)
}
}
}
}

publicIP := runtimeconfig.TryDiscoverPublicIP()
if publicIP != "" {
ipAddresses = append(ipAddresses, net.ParseIP(publicIP))
}

return ipAddresses, nil
}
Loading