mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: support multiple certificates in coder server and helm (#4150)
This commit is contained in:
+145
-13
@@ -21,6 +21,7 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -240,20 +241,64 @@ func TestServer(t *testing.T) {
|
||||
err := root.ExecuteContext(ctx)
|
||||
require.Error(t, err)
|
||||
})
|
||||
t.Run("TLSNoCertFile", func(t *testing.T) {
|
||||
t.Run("TLSInvalid", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
|
||||
root, _ := clitest.New(t,
|
||||
"server",
|
||||
"--in-memory",
|
||||
"--address", ":0",
|
||||
"--tls-enable",
|
||||
"--cache-dir", t.TempDir(),
|
||||
)
|
||||
err := root.ExecuteContext(ctx)
|
||||
require.Error(t, err)
|
||||
cert1Path, key1Path := generateTLSCertificate(t)
|
||||
cert2Path, key2Path := generateTLSCertificate(t)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "NoCertAndKey",
|
||||
args: []string{"--tls-enable"},
|
||||
errContains: "--tls-cert-file is required when tls is enabled",
|
||||
},
|
||||
{
|
||||
name: "NoCert",
|
||||
args: []string{"--tls-enable", "--tls-key-file", key1Path},
|
||||
errContains: "--tls-cert-file and --tls-key-file must be used the same amount of times",
|
||||
},
|
||||
{
|
||||
name: "NoKey",
|
||||
args: []string{"--tls-enable", "--tls-cert-file", cert1Path},
|
||||
errContains: "--tls-cert-file and --tls-key-file must be used the same amount of times",
|
||||
},
|
||||
{
|
||||
name: "MismatchedCount",
|
||||
args: []string{"--tls-enable", "--tls-cert-file", cert1Path, "--tls-key-file", key1Path, "--tls-cert-file", cert2Path},
|
||||
errContains: "--tls-cert-file and --tls-key-file must be used the same amount of times",
|
||||
},
|
||||
{
|
||||
name: "MismatchedCertAndKey",
|
||||
args: []string{"--tls-enable", "--tls-cert-file", cert1Path, "--tls-key-file", key2Path},
|
||||
errContains: "load TLS key pair",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
c := c
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
|
||||
args := []string{
|
||||
"server",
|
||||
"--in-memory",
|
||||
"--address", ":0",
|
||||
"--cache-dir", t.TempDir(),
|
||||
}
|
||||
args = append(args, c.args...)
|
||||
root, _ := clitest.New(t, args...)
|
||||
err := root.ExecuteContext(ctx)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, c.errContains)
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("TLSValid", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -293,6 +338,86 @@ func TestServer(t *testing.T) {
|
||||
cancelFunc()
|
||||
require.NoError(t, <-errC)
|
||||
})
|
||||
t.Run("TLSValidMultiple", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
|
||||
cert1Path, key1Path := generateTLSCertificate(t, "alpaca.com")
|
||||
cert2Path, key2Path := generateTLSCertificate(t, "*.llama.com")
|
||||
root, cfg := clitest.New(t,
|
||||
"server",
|
||||
"--in-memory",
|
||||
"--address", ":0",
|
||||
"--tls-enable",
|
||||
"--tls-cert-file", cert1Path,
|
||||
"--tls-key-file", key1Path,
|
||||
"--tls-cert-file", cert2Path,
|
||||
"--tls-key-file", key2Path,
|
||||
"--cache-dir", t.TempDir(),
|
||||
)
|
||||
errC := make(chan error, 1)
|
||||
go func() {
|
||||
errC <- root.ExecuteContext(ctx)
|
||||
}()
|
||||
accessURL := waitAccessURL(t, cfg)
|
||||
require.Equal(t, "https", accessURL.Scheme)
|
||||
originalHost := accessURL.Host
|
||||
|
||||
var (
|
||||
expectAddr string
|
||||
dials int64
|
||||
)
|
||||
client := codersdk.New(accessURL)
|
||||
client.HTTPClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
atomic.AddInt64(&dials, 1)
|
||||
assert.Equal(t, expectAddr, addr)
|
||||
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Always connect to the accessURL ip:port regardless of
|
||||
// hostname.
|
||||
conn, err := tls.Dial(network, originalHost, &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
//nolint:gosec
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: host,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We can't call conn.VerifyHostname because it requires
|
||||
// that the certificates are valid, so we call
|
||||
// VerifyHostname on the first certificate instead.
|
||||
require.Len(t, conn.ConnectionState().PeerCertificates, 1)
|
||||
err = conn.ConnectionState().PeerCertificates[0].VerifyHostname(host)
|
||||
assert.NoError(t, err, "invalid cert common name")
|
||||
return conn, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Use the first certificate and hostname.
|
||||
client.URL.Host = "alpaca.com:443"
|
||||
expectAddr = "alpaca.com:443"
|
||||
_, err := client.HasFirstUser(ctx)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, 1, atomic.LoadInt64(&dials))
|
||||
|
||||
// Use the second certificate (wildcard) and hostname.
|
||||
client.URL.Host = "hi.llama.com:443"
|
||||
expectAddr = "hi.llama.com:443"
|
||||
_, err = client.HasFirstUser(ctx)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, 2, atomic.LoadInt64(&dials))
|
||||
|
||||
cancelFunc()
|
||||
require.NoError(t, <-errC)
|
||||
})
|
||||
// This cannot be ran in parallel because it uses a signal.
|
||||
//nolint:paralleltest
|
||||
t.Run("Shutdown", func(t *testing.T) {
|
||||
@@ -480,16 +605,22 @@ func TestServer(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func generateTLSCertificate(t testing.TB) (certPath, keyPath string) {
|
||||
func generateTLSCertificate(t testing.TB, commonName ...string) (certPath, keyPath string) {
|
||||
dir := t.TempDir()
|
||||
|
||||
commonNameStr := "localhost"
|
||||
if len(commonName) > 0 {
|
||||
commonNameStr = commonName[0]
|
||||
}
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Acme Co"},
|
||||
CommonName: commonNameStr,
|
||||
},
|
||||
DNSNames: []string{commonNameStr},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour * 24 * 180),
|
||||
|
||||
@@ -498,6 +629,7 @@ func generateTLSCertificate(t testing.TB) (certPath, keyPath string) {
|
||||
BasicConstraintsValid: true,
|
||||
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
||||
require.NoError(t, err)
|
||||
certFile, err := os.CreateTemp(dir, "")
|
||||
|
||||
Reference in New Issue
Block a user