From dd19b87e8d098d587bc4a4eca218011ddbf2fe43 Mon Sep 17 00:00:00 2001 From: nhyatt Date: Mon, 13 Mar 2023 17:46:09 -0500 Subject: [PATCH] webserver will now automatically create a certificate for itself if no certificates are defined. --- cmd/webhook/httpServer.go | 32 +++++++++- internal/certificate/create-ca.go | 68 +++++++++++++++++++++ internal/certificate/create-cert.go | 71 ++++++++++++++++++++++ internal/certificate/create-server-cert.go | 42 +++++++++++++ internal/config/config.go | 33 ++-------- internal/config/initialize.go | 2 +- 6 files changed, 218 insertions(+), 30 deletions(-) create mode 100644 internal/certificate/create-ca.go create mode 100644 internal/certificate/create-cert.go create mode 100644 internal/certificate/create-server-cert.go diff --git a/cmd/webhook/httpServer.go b/cmd/webhook/httpServer.go index c4e609c..4b0dbb3 100644 --- a/cmd/webhook/httpServer.go +++ b/cmd/webhook/httpServer.go @@ -1,7 +1,9 @@ package main import ( + "crypto/tls" "log" + "mutating-webhook/internal/certificate" "mutating-webhook/internal/config" "net/http" "strconv" @@ -21,7 +23,19 @@ func crossSiteOrigin(w http.ResponseWriter) { w.Header().Add("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization, X-API-Token") } +func strictTransport(w http.ResponseWriter) { + w.Header().Add("Strict-Transport-Security", "max-age=63072000") +} + func httpServer(cfg *config.Config) { + var serverCertificate tls.Certificate + if config.DefaultConfig().WebServerCertificate == "" || cfg.WebServerKey == "" { + log.Printf("[INFO] No webserver certificate configured, automatically generating self signed certificate.") + serverCertificate = certificate.CreateServerCert() + } else { + // read certificate from files + // check for errors + } path := http.NewServeMux() connection := &http.Server{ @@ -30,6 +44,20 @@ func httpServer(cfg *config.Config) { ReadTimeout: time.Duration(cfg.WebServerReadTimeout) * time.Second, WriteTimeout: time.Duration(cfg.WebServerWriteTimeout) * time.Second, IdleTimeout: time.Duration(cfg.WebServerIdleTimeout) * time.Second, + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + }, + Certificates: []tls.Certificate{ + serverCertificate, + }, + }, } // healthcheck @@ -39,7 +67,7 @@ func httpServer(cfg *config.Config) { // web root path.HandleFunc("/", webRoot) - if err := connection.ListenAndServe(); err != nil { + if err := connection.ListenAndServeTLS("", ""); err != nil { log.Fatalf("[ERROR] %s\n", err) } } @@ -47,6 +75,7 @@ func httpServer(cfg *config.Config) { func webRoot(w http.ResponseWriter, r *http.Request) { httpAccessLog(r) crossSiteOrigin(w) + strictTransport(w) switch { case strings.ToLower(r.Method) != "get": @@ -63,6 +92,7 @@ func webRoot(w http.ResponseWriter, r *http.Request) { func webHealthCheck(w http.ResponseWriter, r *http.Request) { httpAccessLog(r) crossSiteOrigin(w) + strictTransport(w) if strings.ToLower(r.Method) == "get" { tmpltHealthCheck(w) diff --git a/internal/certificate/create-ca.go b/internal/certificate/create-ca.go new file mode 100644 index 0000000..f5f169f --- /dev/null +++ b/internal/certificate/create-ca.go @@ -0,0 +1,68 @@ +package certificate + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "strconv" + "time" +) + +func CreateCA() ([]byte, []byte, []byte, error) { + serial, _ := strconv.ParseInt(time.Now().Format("20060102150405"), 10, 64) + ca := &x509.Certificate{ + SerialNumber: big.NewInt(serial), + Subject: pkix.Name{ + Organization: []string{"Kubernetes Mutating Webserver CA"}, + Country: []string{"K8S"}, + Province: []string{"Cluster Service"}, + Locality: []string{"Cluster Local"}, + //StreetAddress: []string{""}, + //PostalCode: []string{""}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageClientAuth, + x509.ExtKeyUsageServerAuth, + }, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + SignatureAlgorithm: x509.SHA384WithRSA, + } + + keyPair, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return []byte(""), []byte(""), []byte(""), err + } + + certBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &keyPair.PublicKey, keyPair) + if err != nil { + return []byte(""), []byte(""), []byte(""), err + } + + c := new(bytes.Buffer) + pem.Encode(c, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }) + + k := new(bytes.Buffer) + pem.Encode(k, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(keyPair), + }) + + p := new(bytes.Buffer) + pem.Encode(p, &pem.Block{ + Type: "PUBLIC KEY", + Bytes: x509.MarshalPKCS1PublicKey(&keyPair.PublicKey), + }) + + return c.Bytes(), k.Bytes(), p.Bytes(), nil +} diff --git a/internal/certificate/create-cert.go b/internal/certificate/create-cert.go new file mode 100644 index 0000000..5a3dd2a --- /dev/null +++ b/internal/certificate/create-cert.go @@ -0,0 +1,71 @@ +package certificate + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "strconv" + "time" +) + +func CreateCert() ([]byte, []byte, []byte, error) { + serial, _ := strconv.ParseInt(time.Now().Format("20060102150405"), 10, 64) + ca := &x509.Certificate{ + SerialNumber: big.NewInt(serial + 1), + Subject: pkix.Name{ + Organization: []string{"Kubernetes Mutating Webserver"}, + Country: []string{"K8S"}, + Province: []string{"Cluster Service"}, + Locality: []string{"Cluster Local"}, + //StreetAddress: []string{""}, + //PostalCode: []string{""}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 6, 0), + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageClientAuth, + x509.ExtKeyUsageServerAuth, + }, + DNSNames: []string{ + "svc.cluster.local", + "*.svc.cluster.local", + }, + SubjectKeyId: []byte{1, 2, 3, 4, 6}, + KeyUsage: x509.KeyUsageDigitalSignature, + SignatureAlgorithm: x509.SHA384WithRSA, + } + + keyPair, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return []byte(""), []byte(""), []byte(""), err + } + + certBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &keyPair.PublicKey, keyPair) + if err != nil { + return []byte(""), []byte(""), []byte(""), err + } + + c := new(bytes.Buffer) + pem.Encode(c, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }) + + k := new(bytes.Buffer) + pem.Encode(k, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(keyPair), + }) + + p := new(bytes.Buffer) + pem.Encode(p, &pem.Block{ + Type: "PUBLIC KEY", + Bytes: x509.MarshalPKCS1PublicKey(&keyPair.PublicKey), + }) + + return c.Bytes(), k.Bytes(), p.Bytes(), nil +} diff --git a/internal/certificate/create-server-cert.go b/internal/certificate/create-server-cert.go new file mode 100644 index 0000000..e77dfdf --- /dev/null +++ b/internal/certificate/create-server-cert.go @@ -0,0 +1,42 @@ +package certificate + +import ( + "bytes" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "log" +) + +func CreateServerCert() tls.Certificate { + caCertPem, caPrivKeyPem, _, _ := CreateCA() + certCertPem, certPrivKeyPem, certPublicKeyPem, _ := CreateCert() + + caCertBlob, _ := pem.Decode(caCertPem) + caCert, _ := x509.ParseCertificate(caCertBlob.Bytes) + caPrivKeyBlob, _ := pem.Decode(caPrivKeyPem) + caPrivKey, _ := x509.ParsePKCS1PrivateKey(caPrivKeyBlob.Bytes) + certCertBlob, _ := pem.Decode(certCertPem) + certCert, _ := x509.ParseCertificate(certCertBlob.Bytes) + certPublicKeyBlob, _ := pem.Decode(certPublicKeyPem) + certPublicKey, _ := x509.ParsePKCS1PublicKey(certPublicKeyBlob.Bytes) + + signedCert, err := x509.CreateCertificate(rand.Reader, certCert, caCert, certPublicKey, caPrivKey) + if err != nil { + log.Fatalf("[FATAL] CreateCertificate: %v", err) + } + + serverCertPem := new(bytes.Buffer) + pem.Encode(serverCertPem, &pem.Block{ + Type: "CERTIFICATE", + Bytes: signedCert, + }) + + serverCert, err := tls.X509KeyPair(append(serverCertPem.Bytes(), caCertPem...), certPrivKeyPem) + if err != nil { + log.Fatalf("[FATAL] x509KeyPair: %v", err) + } + + return serverCert +} diff --git a/internal/config/config.go b/internal/config/config.go index 561bc27..e0fac50 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,12 +18,14 @@ type Config struct { TZoneUTC *time.Location `ignored:"true"` // logging - LogLevel int `env:"LOG_LEVEL" default:"50"` - Log *logutils.LevelFilter + LogLevel int `env:"LOG_LEVEL" default:"50"` + Log *logutils.LevelFilter `ignored:"true"` // webserver - WebServerPort int `env:"webserver_port" default:"8080"` + WebServerPort int `env:"webserver_port" default:"8443"` WebServerIP string `env:"webserver_ip" default:"0.0.0.0"` + WebServerCertificate string `env:"webserver_cert"` + WebServerKey string `env:"webserver_key"` WebServerReadTimeout int `env:"webserver_read_timeout" default:"5"` WebServerWriteTimeout int `env:"webserver_write_timeout" default:"1"` WebServerIdleTimeout int `env:"webserver_idle_timeout" default:"2"` @@ -39,31 +41,6 @@ func DefaultConfig() *Config { } } -/* -func (cfg *Config) validate() error { - checks := []struct { - bad bool - errMsg string - }{ - {cfg.TimeFormat == "", "no TimeFormat specified"}, - {cfg.LogLevel == 0, "no LogLevel specified"}, - {cfg.WebServerPort == 0, "no WebServer.Port specified"}, - {cfg.WebServerIP == "", "no WebServer.IP specified"}, - {cfg.WebServerReadTimeout == 0, "no WebServer.ReadTimeout specified"}, - {cfg.WebServerWriteTimeout == 0, "no WebServer.WriteTimeout specified"}, - {cfg.WebServerIdleTimeout == 0, "no WebServer.IdleTimeout specified"}, - } - - for _, check := range checks { - if check.bad { - return fmt.Errorf("invalid config: %s", check.errMsg) - } - } - - return nil -} -*/ - func (cfg *Config) setLogLevel() { switch { case cfg.LogLevel <= 20: diff --git a/internal/config/initialize.go b/internal/config/initialize.go index 14cde12..a485d02 100644 --- a/internal/config/initialize.go +++ b/internal/config/initialize.go @@ -73,6 +73,6 @@ func Init() *Config { // print running config cfg.printRunningConfig(cfgInfo) - log.Println("[INFO] initialization complete") + log.Println("[INFO] initialization sequence complete") return cfg }