Skip to content

Commit 5c52ca8

Browse files
authored
add tls configuration (#2189)
* Add TLS configuration
1 parent 2753604 commit 5c52ca8

File tree

4 files changed

+173
-16
lines changed

4 files changed

+173
-16
lines changed

cmd/installer/cli/install.go

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"context"
5+
"crypto/tls"
56
"encoding/json"
67
"errors"
78
"fmt"
@@ -27,6 +28,7 @@ import (
2728
ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1"
2829
"github.com/replicatedhq/embedded-cluster/kinds/types"
2930
newconfig "github.com/replicatedhq/embedded-cluster/pkg-new/config"
31+
"github.com/replicatedhq/embedded-cluster/pkg-new/tlsutils"
3032
"github.com/replicatedhq/embedded-cluster/pkg/addons"
3133
"github.com/replicatedhq/embedded-cluster/pkg/addons/adminconsole"
3234
"github.com/replicatedhq/embedded-cluster/pkg/addons/embeddedclusteroperator"
@@ -85,6 +87,9 @@ type InstallCmdFlags struct {
8587
// guided UI flags
8688
managerPort int
8789
guidedUI bool
90+
certFile string
91+
keyFile string
92+
hostname string
8893
}
8994

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

211219
if err := cmd.Flags().MarkHidden("guided-ui"); err != nil {
212220
return err
213221
}
214222
if err := cmd.Flags().MarkHidden("manager-port"); err != nil {
215223
return err
216224
}
225+
if err := cmd.Flags().MarkHidden("cert-file"); err != nil {
226+
return err
227+
}
228+
if err := cmd.Flags().MarkHidden("key-file"); err != nil {
229+
return err
230+
}
231+
if err := cmd.Flags().MarkHidden("hostname"); err != nil {
232+
return err
233+
}
217234

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

264-
if err := preRunInstallAPI(cmd.Context(), flags.adminConsolePassword, flags.managerPort, configChan); err != nil {
281+
// this is necessary because the api listens on all interfaces,
282+
// and we only know the interface to use when the user selects it in the ui
283+
ipAddresses, err := netutils.ListAllValidIPAddresses()
284+
if err != nil {
285+
return fmt.Errorf("unable to list all valid IP addresses: %w", err)
286+
}
287+
288+
cert, err := tlsutils.GetCertificate(tlsutils.Config{
289+
CertFile: flags.certFile,
290+
KeyFile: flags.keyFile,
291+
Hostname: flags.hostname,
292+
IPAddresses: ipAddresses,
293+
})
294+
if err != nil {
295+
return fmt.Errorf("get tls certificate: %w", err)
296+
}
297+
298+
if err := preRunInstallAPI(cmd.Context(), cert, flags.adminConsolePassword, flags.managerPort, configChan); err != nil {
265299
return fmt.Errorf("unable to start install API: %w", err)
266300
}
267301

268302
// TODO: fix this message
269303
logrus.Info("")
270-
logrus.Infof("Visit %s to configure your cluster", getManagerURL(flags.managerPort))
304+
logrus.Infof("Visit %s to configure your cluster", getManagerURL(flags.hostname, flags.managerPort))
271305

272306
installConfig, ok := <-configChan
273307
if !ok {
@@ -363,7 +397,7 @@ func preRunInstall(cmd *cobra.Command, flags *InstallCmdFlags) error {
363397
return nil
364398
}
365399

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

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

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

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

414448
server := &http.Server{
415-
Handler: router,
449+
Handler: router,
450+
TLSConfig: tlsutils.GetTLSConfig(cert),
416451
}
417452

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

424-
logrus.Debugf("Install API listening on %s", listener.Addr().String())
425-
return server.Serve(listener)
459+
return server.ServeTLS(listener, "", "")
426460
}
427461

428462
func waitForInstallAPI(ctx context.Context, addr string) error {
429463
httpClient := http.Client{
430464
Timeout: 2 * time.Second,
431465
Transport: &http.Transport{
432466
Proxy: nil,
467+
TLSClientConfig: &tls.Config{
468+
InsecureSkipVerify: true,
469+
},
433470
},
434471
}
435472
timeout := time.After(10 * time.Second)
@@ -442,7 +479,7 @@ func waitForInstallAPI(ctx context.Context, addr string) error {
442479
}
443480
return fmt.Errorf("install API did not start in time")
444481
case <-time.Tick(1 * time.Second):
445-
resp, err := httpClient.Get(fmt.Sprintf("http://%s/api/health", addr))
482+
resp, err := httpClient.Get(fmt.Sprintf("https://%s/api/health", addr))
446483
if err != nil {
447484
lastErr = fmt.Errorf("unable to connect to install API: %w", err)
448485
} else if resp.StatusCode == http.StatusOK {
@@ -578,7 +615,7 @@ func runInstall(ctx context.Context, name string, flags InstallCmdFlags, metrics
578615
return fmt.Errorf("unable to mark ui install complete: %w", err)
579616
}
580617
} else {
581-
if err := printSuccessMessage(flags.license, flags.networkInterface); err != nil {
618+
if err := printSuccessMessage(flags.license, flags.hostname, flags.networkInterface); err != nil {
582619
return err
583620
}
584621
}
@@ -1484,8 +1521,8 @@ func copyLicenseFileToDataDir(licenseFile, dataDir string) error {
14841521
return nil
14851522
}
14861523

1487-
func printSuccessMessage(license *kotsv1beta1.License, networkInterface string) error {
1488-
adminConsoleURL := getAdminConsoleURL(networkInterface, runtimeconfig.AdminConsolePort())
1524+
func printSuccessMessage(license *kotsv1beta1.License, hostname string, networkInterface string) error {
1525+
adminConsoleURL := getAdminConsoleURL(hostname, networkInterface, runtimeconfig.AdminConsolePort())
14891526

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

1518-
func getManagerURL(port int) string {
1555+
func getManagerURL(hostname string, port int) string {
1556+
if hostname != "" {
1557+
return fmt.Sprintf("https://%s:%v", hostname, port)
1558+
}
15191559
ipaddr := runtimeconfig.TryDiscoverPublicIP()
15201560
if ipaddr == "" {
15211561
if addr := os.Getenv("EC_PUBLIC_ADDRESS"); addr != "" {
@@ -1525,10 +1565,13 @@ func getManagerURL(port int) string {
15251565
ipaddr = "NODE-IP-ADDRESS"
15261566
}
15271567
}
1528-
return fmt.Sprintf("http://%s:%v", ipaddr, port)
1568+
return fmt.Sprintf("https://%s:%v", ipaddr, port)
15291569
}
15301570

1531-
func getAdminConsoleURL(networkInterface string, port int) string {
1571+
func getAdminConsoleURL(hostname string, networkInterface string, port int) string {
1572+
if hostname != "" {
1573+
return fmt.Sprintf("http://%s:%v", hostname, port)
1574+
}
15321575
ipaddr := runtimeconfig.TryDiscoverPublicIP()
15331576
if ipaddr == "" {
15341577
var err error

cmd/installer/cli/restore.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1579,7 +1579,7 @@ func waitForAdditionalNodes(ctx context.Context, highAvailability bool, networkI
15791579
return fmt.Errorf("unable to create kube client: %w", err)
15801580
}
15811581

1582-
adminConsoleURL := getAdminConsoleURL(networkInterface, runtimeconfig.AdminConsolePort())
1582+
adminConsoleURL := getAdminConsoleURL("", networkInterface, runtimeconfig.AdminConsolePort())
15831583

15841584
successColor := "\033[32m"
15851585
colorReset := "\033[0m"

pkg-new/tlsutils/tls.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package tlsutils
2+
3+
import (
4+
"crypto/tls"
5+
"fmt"
6+
"net"
7+
8+
"github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig"
9+
"github.com/sirupsen/logrus"
10+
certutil "k8s.io/client-go/util/cert"
11+
)
12+
13+
var (
14+
// TLSCipherSuites defines the allowed cipher suites for TLS connections
15+
TLSCipherSuites = []uint16{
16+
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
17+
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
18+
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
19+
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
20+
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
21+
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
22+
}
23+
)
24+
25+
// Config represents TLS configuration options
26+
type Config struct {
27+
CertFile string
28+
KeyFile string
29+
Hostname string
30+
IPAddresses []net.IP
31+
}
32+
33+
// GetCertificate returns a TLS certificate based on the provided configuration.
34+
// If cert and key files are provided, it uses those. Otherwise, it generates a self-signed certificate.
35+
func GetCertificate(cfg Config) (tls.Certificate, error) {
36+
if cfg.CertFile != "" && cfg.KeyFile != "" {
37+
logrus.Debugf("Using TLS configuration with cert file: %s and key file: %s", cfg.CertFile, cfg.KeyFile)
38+
return tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile)
39+
}
40+
41+
hostname, altNames := generateCertHostnames(cfg.Hostname)
42+
43+
// Generate a new self-signed cert
44+
certData, keyData, err := certutil.GenerateSelfSignedCertKey(hostname, cfg.IPAddresses, altNames)
45+
if err != nil {
46+
return tls.Certificate{}, fmt.Errorf("generate self-signed cert: %w", err)
47+
}
48+
49+
cert, err := tls.X509KeyPair(certData, keyData)
50+
if err != nil {
51+
return tls.Certificate{}, fmt.Errorf("create TLS certificate: %w", err)
52+
}
53+
54+
logrus.Debugf("Using self-signed TLS certificate for hostname: %s", hostname)
55+
return cert, nil
56+
}
57+
58+
// GetTLSConfig returns a TLS configuration with the provided certificate
59+
func GetTLSConfig(cert tls.Certificate) *tls.Config {
60+
return &tls.Config{
61+
MinVersion: tls.VersionTLS12,
62+
CipherSuites: TLSCipherSuites,
63+
Certificates: []tls.Certificate{cert},
64+
}
65+
}
66+
67+
func generateCertHostnames(hostname string) (string, []string) {
68+
namespace := runtimeconfig.KotsadmNamespace
69+
70+
if hostname == "" {
71+
hostname = fmt.Sprintf("kotsadm.%s.svc.cluster.local", namespace)
72+
}
73+
74+
altNames := []string{
75+
"kotsadm",
76+
fmt.Sprintf("kotsadm.%s", namespace),
77+
fmt.Sprintf("kotsadm.%s.svc", namespace),
78+
fmt.Sprintf("kotsadm.%s.svc.cluster", namespace),
79+
fmt.Sprintf("kotsadm.%s.svc.cluster.local", namespace),
80+
}
81+
82+
return hostname, altNames
83+
}

pkg/netutils/ips.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"fmt"
55
"net"
66
"strings"
7+
8+
"github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig"
79
)
810

911
// adapted from https://github.com/k0sproject/k0s/blob/v1.30.4%2Bk0s.0/internal/pkg/iface/iface.go#L61
@@ -114,3 +116,32 @@ func firstValidIPNet(i net.Interface) (*net.IPNet, error) {
114116
}
115117
return nil, fmt.Errorf("could not find any non-local, non podnetwork ipv4 addresses")
116118
}
119+
120+
func ListAllValidIPAddresses() ([]net.IP, error) {
121+
ipAddresses := []net.IP{}
122+
123+
ifs, err := ListValidNetworkInterfaces()
124+
if err != nil {
125+
return nil, fmt.Errorf("list valid network interfaces: %w", err)
126+
}
127+
for _, i := range ifs {
128+
addrs, err := i.Addrs()
129+
if err != nil {
130+
return nil, fmt.Errorf("get addresses: %w", err)
131+
}
132+
for _, addr := range addrs {
133+
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
134+
if ipnet.IP.To4() != nil {
135+
ipAddresses = append(ipAddresses, ipnet.IP)
136+
}
137+
}
138+
}
139+
}
140+
141+
publicIP := runtimeconfig.TryDiscoverPublicIP()
142+
if publicIP != "" {
143+
ipAddresses = append(ipAddresses, net.ParseIP(publicIP))
144+
}
145+
146+
return ipAddresses, nil
147+
}

0 commit comments

Comments
 (0)