|
5 | 5 | "crypto/tls"
|
6 | 6 | "crypto/x509"
|
7 | 7 | "database/sql"
|
8 |
| - "encoding/pem" |
9 | 8 | "errors"
|
10 | 9 | "fmt"
|
11 | 10 | "io"
|
@@ -106,11 +105,11 @@ func Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, error))
|
106 | 105 | telemetryEnable bool
|
107 | 106 | telemetryTraceEnable bool
|
108 | 107 | telemetryURL string
|
109 |
| - tlsCertFile string |
| 108 | + tlsCertFiles []string |
110 | 109 | tlsClientCAFile string
|
111 | 110 | tlsClientAuth string
|
112 | 111 | tlsEnable bool
|
113 |
| - tlsKeyFile string |
| 112 | + tlsKeyFiles []string |
114 | 113 | tlsMinVersion string
|
115 | 114 | tunnel bool
|
116 | 115 | traceEnable bool
|
@@ -221,7 +220,7 @@ func Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, error))
|
221 | 220 | defer listener.Close()
|
222 | 221 |
|
223 | 222 | if tlsEnable {
|
224 |
| - listener, err = configureTLS(listener, tlsMinVersion, tlsClientAuth, tlsCertFile, tlsKeyFile, tlsClientCAFile) |
| 223 | + listener, err = configureServerTLS(listener, tlsMinVersion, tlsClientAuth, tlsCertFiles, tlsKeyFiles, tlsClientCAFile) |
225 | 224 | if err != nil {
|
226 | 225 | return xerrors.Errorf("configure tls: %w", err)
|
227 | 226 | }
|
@@ -369,6 +368,7 @@ func Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, error))
|
369 | 368 | AutoImportTemplates: validatedAutoImportTemplates,
|
370 | 369 | MetricsCacheRefreshInterval: metricsCacheRefreshInterval,
|
371 | 370 | AgentStatsRefreshInterval: agentStatRefreshInterval,
|
| 371 | + Experimental: ExperimentalEnabled(cmd), |
372 | 372 | }
|
373 | 373 |
|
374 | 374 | if oauth2GithubClientSecret != "" {
|
@@ -842,17 +842,17 @@ func Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, error))
|
842 | 842 | _ = root.Flags().MarkHidden("telemetry-url")
|
843 | 843 | cliflag.BoolVarP(root.Flags(), &tlsEnable, "tls-enable", "", "CODER_TLS_ENABLE", false,
|
844 | 844 | "Whether TLS will be enabled.")
|
845 |
| - cliflag.StringVarP(root.Flags(), &tlsCertFile, "tls-cert-file", "", "CODER_TLS_CERT_FILE", "", |
846 |
| - "Path to the certificate for TLS. It requires a PEM-encoded file. "+ |
| 845 | + cliflag.StringArrayVarP(root.Flags(), &tlsCertFiles, "tls-cert-file", "", "CODER_TLS_CERT_FILE", []string{}, |
| 846 | + "Path to each certificate for TLS. It requires a PEM-encoded file. "+ |
847 | 847 | "To configure the listener to use a CA certificate, concatenate the primary certificate "+
|
848 | 848 | "and the CA certificate together. The primary certificate should appear first in the combined file.")
|
849 | 849 | cliflag.StringVarP(root.Flags(), &tlsClientCAFile, "tls-client-ca-file", "", "CODER_TLS_CLIENT_CA_FILE", "",
|
850 | 850 | "PEM-encoded Certificate Authority file used for checking the authenticity of client")
|
851 | 851 | cliflag.StringVarP(root.Flags(), &tlsClientAuth, "tls-client-auth", "", "CODER_TLS_CLIENT_AUTH", "request",
|
852 | 852 | `Policy the server will follow for TLS Client Authentication. `+
|
853 | 853 | `Accepted values are "none", "request", "require-any", "verify-if-given", or "require-and-verify"`)
|
854 |
| - cliflag.StringVarP(root.Flags(), &tlsKeyFile, "tls-key-file", "", "CODER_TLS_KEY_FILE", "", |
855 |
| - "Path to the private key for the certificate. It requires a PEM-encoded file") |
| 854 | + cliflag.StringArrayVarP(root.Flags(), &tlsKeyFiles, "tls-key-file", "", "CODER_TLS_KEY_FILE", []string{}, |
| 855 | + "Paths to the private keys for each of the certificates. It requires a PEM-encoded file") |
856 | 856 | cliflag.StringVarP(root.Flags(), &tlsMinVersion, "tls-min-version", "", "CODER_TLS_MIN_VERSION", "tls12",
|
857 | 857 | `Minimum supported version of TLS. Accepted values are "tls10", "tls11", "tls12" or "tls13"`)
|
858 | 858 | cliflag.BoolVarP(root.Flags(), &tunnel, "tunnel", "", "CODER_TUNNEL", false,
|
@@ -1040,7 +1040,32 @@ func printLogo(cmd *cobra.Command, spooky bool) {
|
1040 | 1040 | _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s - Remote development on your infrastucture\n", cliui.Styles.Bold.Render("Coder "+buildinfo.Version()))
|
1041 | 1041 | }
|
1042 | 1042 |
|
1043 |
| -func configureTLS(listener net.Listener, tlsMinVersion, tlsClientAuth, tlsCertFile, tlsKeyFile, tlsClientCAFile string) (net.Listener, error) { |
| 1043 | +func loadCertificates(tlsCertFiles, tlsKeyFiles []string) ([]tls.Certificate, error) { |
| 1044 | + if len(tlsCertFiles) != len(tlsKeyFiles) { |
| 1045 | + return nil, xerrors.New("--tls-cert-file and --tls-key-file must be used the same amount of times") |
| 1046 | + } |
| 1047 | + if len(tlsCertFiles) == 0 { |
| 1048 | + return nil, xerrors.New("--tls-cert-file is required when tls is enabled") |
| 1049 | + } |
| 1050 | + if len(tlsKeyFiles) == 0 { |
| 1051 | + return nil, xerrors.New("--tls-key-file is required when tls is enabled") |
| 1052 | + } |
| 1053 | + |
| 1054 | + certs := make([]tls.Certificate, len(tlsCertFiles)) |
| 1055 | + for i := range tlsCertFiles { |
| 1056 | + certFile, keyFile := tlsCertFiles[i], tlsKeyFiles[i] |
| 1057 | + cert, err := tls.LoadX509KeyPair(certFile, keyFile) |
| 1058 | + if err != nil { |
| 1059 | + return nil, xerrors.Errorf("load TLS key pair %d (%q, %q): %w", i, certFile, keyFile, err) |
| 1060 | + } |
| 1061 | + |
| 1062 | + certs[i] = cert |
| 1063 | + } |
| 1064 | + |
| 1065 | + return certs, nil |
| 1066 | +} |
| 1067 | + |
| 1068 | +func configureServerTLS(listener net.Listener, tlsMinVersion, tlsClientAuth string, tlsCertFiles, tlsKeyFiles []string, tlsClientCAFile string) (net.Listener, error) { |
1044 | 1069 | tlsConfig := &tls.Config{
|
1045 | 1070 | MinVersion: tls.VersionTLS12,
|
1046 | 1071 | }
|
@@ -1072,36 +1097,31 @@ func configureTLS(listener net.Listener, tlsMinVersion, tlsClientAuth, tlsCertFi
|
1072 | 1097 | return nil, xerrors.Errorf("unrecognized tls client auth: %q", tlsClientAuth)
|
1073 | 1098 | }
|
1074 | 1099 |
|
1075 |
| - if tlsCertFile == "" { |
1076 |
| - return nil, xerrors.New("tls-cert-file is required when tls is enabled") |
1077 |
| - } |
1078 |
| - if tlsKeyFile == "" { |
1079 |
| - return nil, xerrors.New("tls-key-file is required when tls is enabled") |
1080 |
| - } |
1081 |
| - |
1082 |
| - certPEMBlock, err := os.ReadFile(tlsCertFile) |
1083 |
| - if err != nil { |
1084 |
| - return nil, xerrors.Errorf("read file %q: %w", tlsCertFile, err) |
1085 |
| - } |
1086 |
| - keyPEMBlock, err := os.ReadFile(tlsKeyFile) |
| 1100 | + certs, err := loadCertificates(tlsCertFiles, tlsKeyFiles) |
1087 | 1101 | if err != nil {
|
1088 |
| - return nil, xerrors.Errorf("read file %q: %w", tlsKeyFile, err) |
1089 |
| - } |
1090 |
| - keyBlock, _ := pem.Decode(keyPEMBlock) |
1091 |
| - if keyBlock == nil { |
1092 |
| - return nil, xerrors.New("decoded pem is blank") |
1093 |
| - } |
1094 |
| - cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) |
1095 |
| - if err != nil { |
1096 |
| - return nil, xerrors.Errorf("create key pair: %w", err) |
1097 |
| - } |
1098 |
| - tlsConfig.GetCertificate = func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) { |
1099 |
| - return &cert, nil |
| 1102 | + return nil, xerrors.Errorf("load certificates: %w", err) |
1100 | 1103 | }
|
| 1104 | + tlsConfig.GetCertificate = func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) { |
| 1105 | + // If there's only one certificate, return it. |
| 1106 | + if len(certs) == 1 { |
| 1107 | + return &certs[0], nil |
| 1108 | + } |
| 1109 | + |
| 1110 | + // Expensively check which certificate matches the client hello. |
| 1111 | + for _, cert := range certs { |
| 1112 | + cert := cert |
| 1113 | + if err := hi.SupportsCertificate(&cert); err == nil { |
| 1114 | + return &cert, nil |
| 1115 | + } |
| 1116 | + } |
1101 | 1117 |
|
1102 |
| - certPool := x509.NewCertPool() |
1103 |
| - certPool.AppendCertsFromPEM(certPEMBlock) |
1104 |
| - tlsConfig.RootCAs = certPool |
| 1118 | + // Return the first certificate if we have one, or return nil so the |
| 1119 | + // server doesn't fail. |
| 1120 | + if len(certs) > 0 { |
| 1121 | + return &certs[0], nil |
| 1122 | + } |
| 1123 | + return nil, nil //nolint:nilnil |
| 1124 | + } |
1105 | 1125 |
|
1106 | 1126 | if tlsClientCAFile != "" {
|
1107 | 1127 | caPool := x509.NewCertPool()
|
|
0 commit comments