Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit6325a9e

Browse files
authored
feat: support multiple certificates in coder server and helm (#4150)
1 parenta1056bf commit6325a9e

File tree

6 files changed

+294
-78
lines changed

6 files changed

+294
-78
lines changed

‎cli/server.go

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"crypto/tls"
66
"crypto/x509"
77
"database/sql"
8-
"encoding/pem"
98
"errors"
109
"fmt"
1110
"io"
@@ -106,11 +105,11 @@ func Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, error))
106105
telemetryEnablebool
107106
telemetryTraceEnablebool
108107
telemetryURLstring
109-
tlsCertFilestring
108+
tlsCertFiles[]string
110109
tlsClientCAFilestring
111110
tlsClientAuthstring
112111
tlsEnablebool
113-
tlsKeyFilestring
112+
tlsKeyFiles[]string
114113
tlsMinVersionstring
115114
tunnelbool
116115
traceEnablebool
@@ -221,7 +220,7 @@ func Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, error))
221220
deferlistener.Close()
222221

223222
iftlsEnable {
224-
listener,err=configureTLS(listener,tlsMinVersion,tlsClientAuth,tlsCertFile,tlsKeyFile,tlsClientCAFile)
223+
listener,err=configureServerTLS(listener,tlsMinVersion,tlsClientAuth,tlsCertFiles,tlsKeyFiles,tlsClientCAFile)
225224
iferr!=nil {
226225
returnxerrors.Errorf("configure tls: %w",err)
227226
}
@@ -842,17 +841,17 @@ func Server(newAPI func(context.Context, *coderd.Options) (*coderd.API, error))
842841
_=root.Flags().MarkHidden("telemetry-url")
843842
cliflag.BoolVarP(root.Flags(),&tlsEnable,"tls-enable","","CODER_TLS_ENABLE",false,
844843
"Whether TLS will be enabled.")
845-
cliflag.StringVarP(root.Flags(),&tlsCertFile,"tls-cert-file","","CODER_TLS_CERT_FILE","",
846-
"Path tothe certificate for TLS. It requires a PEM-encoded file. "+
844+
cliflag.StringArrayVarP(root.Flags(),&tlsCertFiles,"tls-cert-file","","CODER_TLS_CERT_FILE",[]string{},
845+
"Path toeach certificate for TLS. It requires a PEM-encoded file. "+
847846
"To configure the listener to use a CA certificate, concatenate the primary certificate "+
848847
"and the CA certificate together. The primary certificate should appear first in the combined file.")
849848
cliflag.StringVarP(root.Flags(),&tlsClientCAFile,"tls-client-ca-file","","CODER_TLS_CLIENT_CA_FILE","",
850849
"PEM-encoded Certificate Authority file used for checking the authenticity of client")
851850
cliflag.StringVarP(root.Flags(),&tlsClientAuth,"tls-client-auth","","CODER_TLS_CLIENT_AUTH","request",
852851
`Policy the server will follow for TLS Client Authentication. `+
853852
`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 privatekey for thecertificate. It requires a PEM-encoded file")
853+
cliflag.StringArrayVarP(root.Flags(),&tlsKeyFiles,"tls-key-file","","CODER_TLS_KEY_FILE",[]string{},
854+
"Paths to the privatekeys foreach ofthecertificates. It requires a PEM-encoded file")
856855
cliflag.StringVarP(root.Flags(),&tlsMinVersion,"tls-min-version","","CODER_TLS_MIN_VERSION","tls12",
857856
`Minimum supported version of TLS. Accepted values are "tls10", "tls11", "tls12" or "tls13"`)
858857
cliflag.BoolVarP(root.Flags(),&tunnel,"tunnel","","CODER_TUNNEL",false,
@@ -1040,7 +1039,32 @@ func printLogo(cmd *cobra.Command, spooky bool) {
10401039
_,_=fmt.Fprintf(cmd.OutOrStdout(),"%s - Remote development on your infrastucture\n",cliui.Styles.Bold.Render("Coder "+buildinfo.Version()))
10411040
}
10421041

1043-
funcconfigureTLS(listener net.Listener,tlsMinVersion,tlsClientAuth,tlsCertFile,tlsKeyFile,tlsClientCAFilestring) (net.Listener,error) {
1042+
funcloadCertificates(tlsCertFiles,tlsKeyFiles []string) ([]tls.Certificate,error) {
1043+
iflen(tlsCertFiles)!=len(tlsKeyFiles) {
1044+
returnnil,xerrors.New("--tls-cert-file and --tls-key-file must be used the same amount of times")
1045+
}
1046+
iflen(tlsCertFiles)==0 {
1047+
returnnil,xerrors.New("--tls-cert-file is required when tls is enabled")
1048+
}
1049+
iflen(tlsKeyFiles)==0 {
1050+
returnnil,xerrors.New("--tls-key-file is required when tls is enabled")
1051+
}
1052+
1053+
certs:=make([]tls.Certificate,len(tlsCertFiles))
1054+
fori:=rangetlsCertFiles {
1055+
certFile,keyFile:=tlsCertFiles[i],tlsKeyFiles[i]
1056+
cert,err:=tls.LoadX509KeyPair(certFile,keyFile)
1057+
iferr!=nil {
1058+
returnnil,xerrors.Errorf("load TLS key pair %d (%q, %q): %w",i,certFile,keyFile,err)
1059+
}
1060+
1061+
certs[i]=cert
1062+
}
1063+
1064+
returncerts,nil
1065+
}
1066+
1067+
funcconfigureServerTLS(listener net.Listener,tlsMinVersion,tlsClientAuthstring,tlsCertFiles,tlsKeyFiles []string,tlsClientCAFilestring) (net.Listener,error) {
10441068
tlsConfig:=&tls.Config{
10451069
MinVersion:tls.VersionTLS12,
10461070
}
@@ -1072,36 +1096,31 @@ func configureTLS(listener net.Listener, tlsMinVersion, tlsClientAuth, tlsCertFi
10721096
returnnil,xerrors.Errorf("unrecognized tls client auth: %q",tlsClientAuth)
10731097
}
10741098

1075-
iftlsCertFile=="" {
1076-
returnnil,xerrors.New("tls-cert-file is required when tls is enabled")
1077-
}
1078-
iftlsKeyFile=="" {
1079-
returnnil,xerrors.New("tls-key-file is required when tls is enabled")
1080-
}
1081-
1082-
certPEMBlock,err:=os.ReadFile(tlsCertFile)
1083-
iferr!=nil {
1084-
returnnil,xerrors.Errorf("read file %q: %w",tlsCertFile,err)
1085-
}
1086-
keyPEMBlock,err:=os.ReadFile(tlsKeyFile)
1099+
certs,err:=loadCertificates(tlsCertFiles,tlsKeyFiles)
10871100
iferr!=nil {
1088-
returnnil,xerrors.Errorf("read file %q: %w",tlsKeyFile,err)
1089-
}
1090-
keyBlock,_:=pem.Decode(keyPEMBlock)
1091-
ifkeyBlock==nil {
1092-
returnnil,xerrors.New("decoded pem is blank")
1093-
}
1094-
cert,err:=tls.X509KeyPair(certPEMBlock,keyPEMBlock)
1095-
iferr!=nil {
1096-
returnnil,xerrors.Errorf("create key pair: %w",err)
1097-
}
1098-
tlsConfig.GetCertificate=func(chi*tls.ClientHelloInfo) (*tls.Certificate,error) {
1099-
return&cert,nil
1101+
returnnil,xerrors.Errorf("load certificates: %w",err)
11001102
}
1103+
tlsConfig.GetCertificate=func(hi*tls.ClientHelloInfo) (*tls.Certificate,error) {
1104+
// If there's only one certificate, return it.
1105+
iflen(certs)==1 {
1106+
return&certs[0],nil
1107+
}
1108+
1109+
// Expensively check which certificate matches the client hello.
1110+
for_,cert:=rangecerts {
1111+
cert:=cert
1112+
iferr:=hi.SupportsCertificate(&cert);err==nil {
1113+
return&cert,nil
1114+
}
1115+
}
11011116

1102-
certPool:=x509.NewCertPool()
1103-
certPool.AppendCertsFromPEM(certPEMBlock)
1104-
tlsConfig.RootCAs=certPool
1117+
// Return the first certificate if we have one, or return nil so the
1118+
// server doesn't fail.
1119+
iflen(certs)>0 {
1120+
return&certs[0],nil
1121+
}
1122+
returnnil,nil//nolint:nilnil
1123+
}
11051124

11061125
iftlsClientCAFile!="" {
11071126
caPool:=x509.NewCertPool()

‎cli/server_test.go

Lines changed: 145 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"runtime"
2222
"strconv"
2323
"strings"
24+
"sync/atomic"
2425
"testing"
2526
"time"
2627

@@ -240,20 +241,64 @@ func TestServer(t *testing.T) {
240241
err:=root.ExecuteContext(ctx)
241242
require.Error(t,err)
242243
})
243-
t.Run("TLSNoCertFile",func(t*testing.T) {
244+
t.Run("TLSInvalid",func(t*testing.T) {
244245
t.Parallel()
245-
ctx,cancelFunc:=context.WithCancel(context.Background())
246-
defercancelFunc()
247246

248-
root,_:=clitest.New(t,
249-
"server",
250-
"--in-memory",
251-
"--address",":0",
252-
"--tls-enable",
253-
"--cache-dir",t.TempDir(),
254-
)
255-
err:=root.ExecuteContext(ctx)
256-
require.Error(t,err)
247+
cert1Path,key1Path:=generateTLSCertificate(t)
248+
cert2Path,key2Path:=generateTLSCertificate(t)
249+
250+
cases:= []struct {
251+
namestring
252+
args []string
253+
errContainsstring
254+
}{
255+
{
256+
name:"NoCertAndKey",
257+
args: []string{"--tls-enable"},
258+
errContains:"--tls-cert-file is required when tls is enabled",
259+
},
260+
{
261+
name:"NoCert",
262+
args: []string{"--tls-enable","--tls-key-file",key1Path},
263+
errContains:"--tls-cert-file and --tls-key-file must be used the same amount of times",
264+
},
265+
{
266+
name:"NoKey",
267+
args: []string{"--tls-enable","--tls-cert-file",cert1Path},
268+
errContains:"--tls-cert-file and --tls-key-file must be used the same amount of times",
269+
},
270+
{
271+
name:"MismatchedCount",
272+
args: []string{"--tls-enable","--tls-cert-file",cert1Path,"--tls-key-file",key1Path,"--tls-cert-file",cert2Path},
273+
errContains:"--tls-cert-file and --tls-key-file must be used the same amount of times",
274+
},
275+
{
276+
name:"MismatchedCertAndKey",
277+
args: []string{"--tls-enable","--tls-cert-file",cert1Path,"--tls-key-file",key2Path},
278+
errContains:"load TLS key pair",
279+
},
280+
}
281+
282+
for_,c:=rangecases {
283+
c:=c
284+
t.Run(c.name,func(t*testing.T) {
285+
t.Parallel()
286+
ctx,cancelFunc:=context.WithCancel(context.Background())
287+
defercancelFunc()
288+
289+
args:= []string{
290+
"server",
291+
"--in-memory",
292+
"--address",":0",
293+
"--cache-dir",t.TempDir(),
294+
}
295+
args=append(args,c.args...)
296+
root,_:=clitest.New(t,args...)
297+
err:=root.ExecuteContext(ctx)
298+
require.Error(t,err)
299+
require.ErrorContains(t,err,c.errContains)
300+
})
301+
}
257302
})
258303
t.Run("TLSValid",func(t*testing.T) {
259304
t.Parallel()
@@ -293,6 +338,86 @@ func TestServer(t *testing.T) {
293338
cancelFunc()
294339
require.NoError(t,<-errC)
295340
})
341+
t.Run("TLSValidMultiple",func(t*testing.T) {
342+
t.Parallel()
343+
ctx,cancelFunc:=context.WithCancel(context.Background())
344+
defercancelFunc()
345+
346+
cert1Path,key1Path:=generateTLSCertificate(t,"alpaca.com")
347+
cert2Path,key2Path:=generateTLSCertificate(t,"*.llama.com")
348+
root,cfg:=clitest.New(t,
349+
"server",
350+
"--in-memory",
351+
"--address",":0",
352+
"--tls-enable",
353+
"--tls-cert-file",cert1Path,
354+
"--tls-key-file",key1Path,
355+
"--tls-cert-file",cert2Path,
356+
"--tls-key-file",key2Path,
357+
"--cache-dir",t.TempDir(),
358+
)
359+
errC:=make(chanerror,1)
360+
gofunc() {
361+
errC<-root.ExecuteContext(ctx)
362+
}()
363+
accessURL:=waitAccessURL(t,cfg)
364+
require.Equal(t,"https",accessURL.Scheme)
365+
originalHost:=accessURL.Host
366+
367+
var (
368+
expectAddrstring
369+
dialsint64
370+
)
371+
client:=codersdk.New(accessURL)
372+
client.HTTPClient=&http.Client{
373+
Transport:&http.Transport{
374+
DialTLSContext:func(ctx context.Context,network,addrstring) (net.Conn,error) {
375+
atomic.AddInt64(&dials,1)
376+
assert.Equal(t,expectAddr,addr)
377+
378+
host,_,err:=net.SplitHostPort(addr)
379+
require.NoError(t,err)
380+
381+
// Always connect to the accessURL ip:port regardless of
382+
// hostname.
383+
conn,err:=tls.Dial(network,originalHost,&tls.Config{
384+
MinVersion:tls.VersionTLS12,
385+
//nolint:gosec
386+
InsecureSkipVerify:true,
387+
ServerName:host,
388+
})
389+
iferr!=nil {
390+
returnnil,err
391+
}
392+
393+
// We can't call conn.VerifyHostname because it requires
394+
// that the certificates are valid, so we call
395+
// VerifyHostname on the first certificate instead.
396+
require.Len(t,conn.ConnectionState().PeerCertificates,1)
397+
err=conn.ConnectionState().PeerCertificates[0].VerifyHostname(host)
398+
assert.NoError(t,err,"invalid cert common name")
399+
returnconn,nil
400+
},
401+
},
402+
}
403+
404+
// Use the first certificate and hostname.
405+
client.URL.Host="alpaca.com:443"
406+
expectAddr="alpaca.com:443"
407+
_,err:=client.HasFirstUser(ctx)
408+
require.NoError(t,err)
409+
require.EqualValues(t,1,atomic.LoadInt64(&dials))
410+
411+
// Use the second certificate (wildcard) and hostname.
412+
client.URL.Host="hi.llama.com:443"
413+
expectAddr="hi.llama.com:443"
414+
_,err=client.HasFirstUser(ctx)
415+
require.NoError(t,err)
416+
require.EqualValues(t,2,atomic.LoadInt64(&dials))
417+
418+
cancelFunc()
419+
require.NoError(t,<-errC)
420+
})
296421
// This cannot be ran in parallel because it uses a signal.
297422
//nolint:paralleltest
298423
t.Run("Shutdown",func(t*testing.T) {
@@ -480,16 +605,22 @@ func TestServer(t *testing.T) {
480605
})
481606
}
482607

483-
funcgenerateTLSCertificate(t testing.TB) (certPath,keyPathstring) {
608+
funcgenerateTLSCertificate(t testing.TB,commonName...string) (certPath,keyPathstring) {
484609
dir:=t.TempDir()
485610

611+
commonNameStr:="localhost"
612+
iflen(commonName)>0 {
613+
commonNameStr=commonName[0]
614+
}
486615
privateKey,err:=ecdsa.GenerateKey(elliptic.P256(),rand.Reader)
487616
require.NoError(t,err)
488617
template:= x509.Certificate{
489618
SerialNumber:big.NewInt(1),
490619
Subject: pkix.Name{
491620
Organization: []string{"Acme Co"},
621+
CommonName:commonNameStr,
492622
},
623+
DNSNames: []string{commonNameStr},
493624
NotBefore:time.Now(),
494625
NotAfter:time.Now().Add(time.Hour*24*180),
495626

@@ -498,6 +629,7 @@ func generateTLSCertificate(t testing.TB) (certPath, keyPath string) {
498629
BasicConstraintsValid:true,
499630
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
500631
}
632+
501633
derBytes,err:=x509.CreateCertificate(rand.Reader,&template,&template,&privateKey.PublicKey,privateKey)
502634
require.NoError(t,err)
503635
certFile,err:=os.CreateTemp(dir,"")

‎helm/templates/NOTES.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{{- if .Values.coder.tls.secretName }}
2+
3+
WARN: coder.tls.secretName is deprecated and will be removed in a future
4+
release. Please use coder.tls.secretNames instead.
5+
{{- end }}
6+
7+
Enjoy Coder! Please create an issue at https://github.com/coder/coder if you run
8+
into any problems! :)

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp