diff --git a/pkg/config/certs.go b/pkg/config/certs.go index 9244a3430..3ad0c44b1 100644 --- a/pkg/config/certs.go +++ b/pkg/config/certs.go @@ -139,8 +139,19 @@ func (c *Config) selfSignCertificate() ([]byte, []byte, error) { ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, IsCA: true, - DNSNames: []string{c.Hostname}, - IPAddresses: []net.IP{net.ParseIP(c.Address)}, + } + addrIP := net.ParseIP(c.Address) + if !addrIP.IsUnspecified() { + template.IPAddresses = append(template.IPAddresses, addrIP) + } + if ip := net.ParseIP(c.Hostname); ip != nil { + // is an IP literal + if !addrIP.Equal(ip) { + template.IPAddresses = append(template.IPAddresses, ip) + } + } else { + // is a hostname (not an IP literal) + template.DNSNames = append(template.DNSNames, c.Hostname) } certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) diff --git a/pkg/config/certs_test.go b/pkg/config/certs_test.go new file mode 100644 index 000000000..55be69a41 --- /dev/null +++ b/pkg/config/certs_test.go @@ -0,0 +1,104 @@ +package config + +import ( + "bytes" + "crypto/x509" + "net" + "sort" + "testing" +) + +func ipsEqual(a, b []net.IP) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if !a[i].Equal(b[i]) { + return false + } + } + return true +} + +func stringsEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func TestSelfSignCert(t *testing.T) { + tests := []struct { + config Config + err error + dnsNames []string + ips []net.IP + }{ + { + config: Config{ + Address: "127.0.0.1", + Hostname: "127.0.0.1", + }, + dnsNames: []string{}, + ips: []net.IP{net.IPv4(127, 0, 0, 1)}, + }, + { + config: Config{ + Address: "192.0.2.1", + Hostname: "example.com", + }, + dnsNames: []string{"example.com"}, + ips: []net.IP{net.IPv4(192, 0, 2, 1)}, + }, + { + config: Config{ + Address: "::", + Hostname: "2001:db8::1:0", + }, + dnsNames: []string{}, + ips: []net.IP{net.ParseIP("2001:db8::1:0")}, + }, + } + + for _, test := range tests { + certBytes, keyBytes, err := test.config.selfSignCertificate() + if err != nil { + if err != test.err { + t.Errorf("Expected error %v, got %v", test.err, err) + } + continue + } + + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + t.Fatalf("ParseCertificate: %v", err) + } + + key, err := x509.ParsePKCS1PrivateKey(keyBytes) + if err != nil { + t.Fatalf("ParsePKCS1PrivateKey: %v", err) + } + if err := key.Validate(); err != nil { + t.Errorf("private key is invalid: %v", err) + } + + dnsNames := cert.DNSNames + sort.Strings(dnsNames) + if !stringsEqual(dnsNames, test.dnsNames) { + t.Errorf("expected DNSNames %v, got %v", test.dnsNames, dnsNames) + } + + ips := cert.IPAddresses + sort.Slice(ips, func(i, j int) bool { + return bytes.Compare(ips[i].To16(), ips[j].To16()) == -1 + }) + if !ipsEqual(ips, test.ips) { + t.Errorf("expected IPs %v, got %v", test.ips, ips) + } + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 0537aed0b..c05c0bc15 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -18,19 +18,32 @@ package config import ( "fmt" + "net" + "net/url" "path/filepath" + "strconv" "github.com/sirupsen/logrus" ) -// ListenURL returns the URL to listen on. -func (c *Config) ListenURL() string { - return fmt.Sprintf("https://%s/authenticate", c.ListenAddr()) +// ServerURL returns the URL to connect to this server. +func (c *Config) ServerURL() string { + u := url.URL{ + Scheme: "https", + Host: c.ServerAddr(), + Path: "/authenticate", + } + return u.String() +} + +// ServerAddr returns the host and port clients should use for server endpoint. +func (c *Config) ServerAddr() string { + return net.JoinHostPort(c.Hostname, strconv.Itoa(c.HostPort)) } // ListenAddr returns the IP address and port mapping to bind with func (c *Config) ListenAddr() string { - return fmt.Sprintf("%s:%d", c.Hostname, c.HostPort) + return net.JoinHostPort(c.Address, strconv.Itoa(c.HostPort)) } // GenerateFiles will generate the certificate+provate key @@ -57,7 +70,7 @@ func (c *Config) CreateKubeconfig() error { // write a kubeconfig suitable for the API server to call us logrus.WithField("kubeconfigPath", c.GenerateKubeconfigPath).Info("writing webhook kubeconfig file") err = kubeconfigParams{ - ServerURL: c.ListenURL(), + ServerURL: c.ServerURL(), CertificateAuthorityBase64: certToPEMBase64(cert.Certificate[0]), }.writeTo(c.GenerateKubeconfigPath) if err != nil { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 000000000..43c2841b6 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,41 @@ +package config + +import ( + "testing" +) + +func TestServerUrl(t *testing.T) { + tests := []struct { + config Config + expected string + }{ + { + config: Config{ + Hostname: "example.com", + HostPort: 6443, + }, + expected: "https://example.com:6443/authenticate", + }, + { + config: Config{ + Hostname: "127.0.0.1", + HostPort: 8080, + }, + expected: "https://127.0.0.1:8080/authenticate", + }, + { + config: Config{ + Hostname: "2001:db8::1:0", + HostPort: 1234, + }, + expected: "https://[2001:db8::1:0]:1234/authenticate", + }, + } + + for _, test := range tests { + actual := test.config.ServerURL() + if actual != test.expected { + t.Errorf("Expected %q, got %q", test.expected, actual) + } + } +} diff --git a/pkg/config/types.go b/pkg/config/types.go index 97e9a5af1..8f6225dfd 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -87,7 +87,7 @@ type Config struct { // HostPort is the TCP Port on which to listen for authentication checks. HostPort int - // Hostname is the hostname that the server bind to. + // Hostname is the address clients should use for this server. Hostname string // GenerateKubeconfigPath is the output path where a generated webhook diff --git a/pkg/server/server.go b/pkg/server/server.go index 82ddae1d3..b1233034e 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -111,9 +111,6 @@ func New(cfg config.Config, mappers []mapper.Mapper) *Server { logrus.WithField("accountID", account).Infof("mapping IAM Account") } - listenAddr := fmt.Sprintf("%s:%d", c.Address, c.HostPort) - listenURL := fmt.Sprintf("https://%s/authenticate", listenAddr) - cert, err := c.GetOrCreateCertificate() if err != nil { logrus.WithError(err).Fatalf("could not load/generate a certificate") @@ -126,7 +123,7 @@ func New(cfg config.Config, mappers []mapper.Mapper) *Server { } // start a TLS listener with our custom certs - listener, err := tls.Listen("tcp", listenAddr, &tls.Config{ + listener, err := tls.Listen("tcp", c.ListenAddr(), &tls.Config{ MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{*cert}, }) @@ -138,7 +135,7 @@ func New(cfg config.Config, mappers []mapper.Mapper) *Server { errLog := logrus.WithField("http", "error").Writer() defer errLog.Close() - logrus.Infof("listening on %s", listenURL) + logrus.Infof("listening on %s", listener.Addr()) logrus.Infof("reconfigure your apiserver with `--authentication-token-webhook-config-file=%s` to enable (assuming default hostPath mounts)", c.GenerateKubeconfigPath) c.httpServer = http.Server{ ErrorLog: log.New(errLog, "", 0),