From 9b90f2e4f43c6a95009df7ac7d09c00f6a0cc9e4 Mon Sep 17 00:00:00 2001 From: Casey Miller <47993553+camillsir@users.noreply.github.com> Date: Mon, 14 Nov 2022 10:49:34 -0800 Subject: [PATCH] Adding reloading of TLS certificates (#2656) * Adding reloading of TLS certificates * Update server/server.go Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> * Adding test and test data * Set minimum tls version * Adding read header timeout Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- server/server.go | 34 ++++++++++++++++++++++++++++++++-- server/server_test.go | 25 +++++++++++++++++++++++++ testdata/cert.pem | 21 +++++++++++++++++++++ testdata/cert2.pem | 22 ++++++++++++++++++++++ testdata/key.pem | 28 ++++++++++++++++++++++++++++ testdata/key2.pem | 28 ++++++++++++++++++++++++++++ 6 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 testdata/cert.pem create mode 100644 testdata/cert2.pem create mode 100644 testdata/key.pem create mode 100644 testdata/key2.pem diff --git a/server/server.go b/server/server.go index 2f3547c1e..96c8469ba 100644 --- a/server/server.go +++ b/server/server.go @@ -17,6 +17,7 @@ package server import ( "context" + "crypto/tls" "flag" "fmt" "io" @@ -112,6 +113,9 @@ type Server struct { ProjectJobsErrorTemplate templates.TemplateWriter SSLCertFile string SSLKeyFile string + CertLastRefreshTime time.Time + KeyLastRefreshTime time.Time + SSLCert *tls.Certificate Drainer *events.Drainer WebAuthentication bool WebUsername string @@ -890,13 +894,15 @@ func (s *Server) Start() error { s.ProjectCmdOutputHandler.Handle() }() - server := &http.Server{Addr: fmt.Sprintf(":%d", s.Port), Handler: n} // nolint: gosec + tlsConfig := &tls.Config{GetCertificate: s.GetSSLCertificate, MinVersion: tls.VersionTLS12} + + server := &http.Server{Addr: fmt.Sprintf(":%d", s.Port), Handler: n, TLSConfig: tlsConfig, ReadHeaderTimeout: 10 * time.Second} go func() { s.Logger.Info("Atlantis started - listening on port %v", s.Port) var err error if s.SSLCertFile != "" && s.SSLKeyFile != "" { - err = server.ListenAndServeTLS(s.SSLCertFile, s.SSLKeyFile) + err = server.ListenAndServeTLS("", "") } else { err = server.ListenAndServe() } @@ -1012,6 +1018,30 @@ var healthzData = []byte(`{ "status": "ok" }`) +func (s *Server) GetSSLCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) { + certStat, err := os.Stat(s.SSLCertFile) + if err != nil { + return nil, fmt.Errorf("while getting cert file modification time: %w", err) + } + + keyStat, err := os.Stat(s.SSLKeyFile) + if err != nil { + return nil, fmt.Errorf("while getting key file modification time: %w", err) + } + + if s.SSLCert == nil || certStat.ModTime() != s.CertLastRefreshTime || keyStat.ModTime() != s.KeyLastRefreshTime { + cert, err := tls.LoadX509KeyPair(s.SSLCertFile, s.SSLKeyFile) + if err != nil { + return nil, fmt.Errorf("while loading tls cert: %w", err) + } + + s.SSLCert = &cert + s.CertLastRefreshTime = certStat.ModTime() + s.KeyLastRefreshTime = keyStat.ModTime() + } + return s.SSLCert, nil +} + // ParseAtlantisURL parses the user-passed atlantis URL to ensure it is valid // and we can use it in our templates. // It removes any trailing slashes from the path so we can concatenate it diff --git a/server/server_test.go b/server/server_test.go index 0592ae6eb..fe35138b8 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -15,6 +15,7 @@ package server_test import ( "bytes" + "crypto/tls" "errors" "io" "net/http" @@ -163,6 +164,30 @@ func BenchmarkHealthz(b *testing.B) { } } +func TestGetCertificate(t *testing.T) { + s := server.Server{} + clientHelloInfo := &tls.ClientHelloInfo{} + + // Initial certificate load + s.SSLCertFile = "../testdata/cert.pem" + s.SSLKeyFile = "../testdata/key.pem" + cert, err := s.GetSSLCertificate(clientHelloInfo) + Ok(t, err) + + // Certificate reload + s.SSLCertFile = "../testdata/cert2.pem" + s.SSLKeyFile = "../testdata/key2.pem" + s.CertLastRefreshTime = s.CertLastRefreshTime.Add(-1 * time.Second) + s.KeyLastRefreshTime = s.KeyLastRefreshTime.Add(-1 * time.Second) + newCert, err := s.GetSSLCertificate(clientHelloInfo) + + Ok(t, err) + Assert( + t, + !bytes.Equal(bytes.Join(cert.Certificate, nil), bytes.Join(newCert.Certificate, nil)), + "Certificate expected to rotate") +} + func TestParseAtlantisURL(t *testing.T) { cases := []struct { In string diff --git a/testdata/cert.pem b/testdata/cert.pem new file mode 100644 index 000000000..b2adc796d --- /dev/null +++ b/testdata/cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDiDCCAnACCQDOnvpjFkiR7TANBgkqhkiG9w0BAQsFADCBhTELMAkGA1UEBhMC +YXQxETAPBgNVBAgMCGF0bGFudGlzMREwDwYDVQQHDAhhdGxhbnRpczERMA8GA1UE +CgwIYXRsYW50aXMxETAPBgNVBAsMCGF0bGFudGlzMREwDwYDVQQDDAhhdGxhbnRp +czEXMBUGCSqGSIb3DQEJARYIYXRsYW50aXMwHhcNMjIxMTEwMTg1ODAwWhcNMjIx +MjEwMTg1ODAwWjCBhTELMAkGA1UEBhMCYXQxETAPBgNVBAgMCGF0bGFudGlzMREw +DwYDVQQHDAhhdGxhbnRpczERMA8GA1UECgwIYXRsYW50aXMxETAPBgNVBAsMCGF0 +bGFudGlzMREwDwYDVQQDDAhhdGxhbnRpczEXMBUGCSqGSIb3DQEJARYIYXRsYW50 +aXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQ+d1Yhu2MBljro63h +bYJk3NgMjhiHQzvV2Uy9C6UnXZ0pELWa3utYx+rTAJUjOWxfed59qF2IGAAMWmm7 +GQt/Apz0AMtU1uSYzQGYQkWVnAODKRtUC+nBrJYnW4r1zY1/duP74rVuLMFLWhlg +O7XPHbtQK5psYlXLmiyaljWpIMnj3pf/H1MUue+AD9yTpg+sRKscnbNsOVB8NV7Z +mfpeddTkNuQL1d1KqfzF6bKz+zbyrcBz+NHC1SmvCGViRie/nE7UDd5OyHA4WM21 +CDfLvCJxmKG69nFDY8Z8EPu/WGWYndeG0piefdlFpZ8GQTpmxD7dgcZ6M3fnCIdX +Zvp/AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGlib941RVj17ynlH/MQ+R1x01a/ +f0GrhHbM4N2IuwGZ77bS4f/yQMC5YJBDcBOog5v7bR3VM8rCNgYSlZDl2aBzBP+8 +vXDjGQHr4AfV+dgM/6J7xdzQKpfFr7JCR421O9KOoI+rbuk4+HG2JpILR6H305XX +KOkiD3Ywbdvsng0gA3rMxKTTs0XWu8Rki7r227P5p73yNBOwLQHwiewAfDiASJ/P +UfzOEJjRHYe6ivz181HVQVn/4RJa1LbVtCVDk70VBA3N4tGHLb4eyeMcn/do1Hvg +8m//uN6x2s7TeswAEGCoj1LIhjHWIulAwlNCHIcXMguJqvubfhzlxmE601I= +-----END CERTIFICATE----- diff --git a/testdata/cert2.pem b/testdata/cert2.pem new file mode 100644 index 000000000..7fbb62319 --- /dev/null +++ b/testdata/cert2.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDlDCCAnwCCQCGl++WdvHa4jANBgkqhkiG9w0BAQsFADCBizELMAkGA1UEBhMC +YXQxEjAQBgNVBAgMCWF0bGFudGlzMjESMBAGA1UEBwwJYXRsYW50aXMyMRIwEAYD +VQQKDAlhdGxhbnRpczIxEjAQBgNVBAsMCWF0bGFudGlzMjESMBAGA1UEAwwJYXRs +YW50aXMyMRgwFgYJKoZIhvcNAQkBFglhdGxhbnRpczIwHhcNMjIxMTEwMTkwNDIx +WhcNMjIxMjEwMTkwNDIxWjCBizELMAkGA1UEBhMCYXQxEjAQBgNVBAgMCWF0bGFu +dGlzMjESMBAGA1UEBwwJYXRsYW50aXMyMRIwEAYDVQQKDAlhdGxhbnRpczIxEjAQ +BgNVBAsMCWF0bGFudGlzMjESMBAGA1UEAwwJYXRsYW50aXMyMRgwFgYJKoZIhvcN +AQkBFglhdGxhbnRpczIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+ +6bvLyg+VlYT/SU7lXJc2Dmdtehe7C/yckNpl+Zjj/8nkNRYfqJ2g8iwHASOWO7+V +vdg91Ti6eC+OMjg54iSmd2rZQ6734I+hSo3C/l4alkOArjDumQPddynjMiOaB67u +S67+vyBwbQq10BxOgJOhY/wQnLBJhOUhWh+UrjnL1LsET6qQicLwTzt/1cWzXmYS +I8gTs0um+7r5WCB/Raf++KhboCtX3zEj6CV164ucEV65y2vQRIa7PQH+znCHGxbT +nnZdielbJPc0S2hVtL2cRqocKCdEYKY3co27Hgvt/M7byETyLbmxbMK1r4f4icr9 +dyqKeY6aT+PxZ43nggYdAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAJTVkyx5SGnh +J5hKOgseJbh4AvwPB++jsrN+z5FE5A6WR/3zZA2bfUuit5uKLAQ0di3l03bZf+2l +zvxLmmeShOArzkaCQ0IRZdHl7rIaFHignikHnon1/fkBVqOoi+R0Hsn6GhVoQYo/ +C7zxQYyM/57Yw1VOW9vheIhvNgomv4GxaztrGEVT5tG58j9BgKqKYOfsHz07rYXc +S2/iMML0xsgj5vXq9XiNJ0NVT3hD+LBnR5DZG64ITP9AgK22VeYKQrXiPWuzacKC +zn+ptHXFDeqofAS6UoelrQ4ZchIsc48IsWdIW5SI3FOfiao4R0l5T7BqToSyP5V2 +DYLLTHTfIL4= +-----END CERTIFICATE----- diff --git a/testdata/key.pem b/testdata/key.pem new file mode 100644 index 000000000..9b00b63f0 --- /dev/null +++ b/testdata/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQ+d1Yhu2MBljr +o63hbYJk3NgMjhiHQzvV2Uy9C6UnXZ0pELWa3utYx+rTAJUjOWxfed59qF2IGAAM +Wmm7GQt/Apz0AMtU1uSYzQGYQkWVnAODKRtUC+nBrJYnW4r1zY1/duP74rVuLMFL +WhlgO7XPHbtQK5psYlXLmiyaljWpIMnj3pf/H1MUue+AD9yTpg+sRKscnbNsOVB8 +NV7ZmfpeddTkNuQL1d1KqfzF6bKz+zbyrcBz+NHC1SmvCGViRie/nE7UDd5OyHA4 +WM21CDfLvCJxmKG69nFDY8Z8EPu/WGWYndeG0piefdlFpZ8GQTpmxD7dgcZ6M3fn +CIdXZvp/AgMBAAECggEANjUaXaRiajgbSMSkjh1B/bfrsxYI9s1R8B718PPcW2HF +KqnS8eFxWw5As4srJH/4xKtwM1hBKtRO7uVlF8tfWArte73ZAKDdm2VSTJSkSDK4 +FoXLOPn+IOcL7Bmq6ifv1GiaqvQb7ABgA5PTkUrr1lX4CMvGuuanKrFLcK4WLVCD +9+GHiduaV0BfTPA3RmszrA4zLOXlI+5zlGwGNb5pUz914i9Lk0feIYbOXRMx9M+U +FmDrKvasIXguUHGRWZ7D7ugV6j5/BTuCBjbzH6067YkA4NnXfiSKNTkedz+qTkGC +IJXUCPMDzy6TKbdPMx94OKot14f0qaGZx8zZ0taLAQKBgQDwfoT4gYKl+X/3X0aG +IZ5tSaLzAXKdlZ55W8YXCWDUX/b3YULGzILqEiS5RgnRIA0tGbtu3PMXtiol5Iff +e4ywSZGilvvPJBbw17l8iC3Y/rnHp6wlOhVusdjozBhOuc3CQj3k56p0OXyahmzN +eHp/zsOMimIeef2ZlskmvXZqjwKBgQDecx8VQoi8Ph8/IDD/2P6wIU73qv57AY8K +pRQqRX3zffEN6nr0Xnlb44Oy/zA/5/7FNcEF4D2jhPQF8zkOll5tf0HlbVAVIlfC +F5o8t0S05Uj5a6zAkc34bCV+oWwvCmxk5vCvDl2POAiO+dqYxom4bFsxRmMOf1VC +hh5YDXcpEQKBgQChc36XSnLINCipjIfO8nDmU6IWW6lzi4d5V5gzzPL5gHdO+jeX +OKLGu2l2DEP45fiSh4ziT2jPSVcgWzywVsRLcQhZS90+4a6Y/2oh5VZKMC/Ojo0t +7MGIr9K77pB/AZPVzxy4OKKhJhq1rnsKsdAjT07OYfSfGyyaWLUv0c/WlwKBgQDR +pYOk4LjHWHDQaIFljtexnSK0TgZaXUS3To8rq6Shh49YgyVwC12q2Uh0uQZ7JCU7 +LYcGB6lv48yrkueyNMs3vRiYpiY0VNKKjP4CvOJW7kSRNQZx0rhgqWPI7U9tIhC4 +I+KvyQUqBjAit51qIKsJEa38SY7vydfLw2TzrXUhUQKBgBgxh9csF9JQfMRKO1Nv +g5YWGFHdujl1Y2Qnf9nWHhQKy2wmCCoD0a0aRs6p0tyZXyqJfyBrPBEGF3VUONqf +OK1b5tSj3w+APFW0hNFQABE8lRYBA/Qq6dAf+pTnDF0+ak9DKzlafhFI7w0dOZU2 +bYCXCqkOjIcnONDRDr04jE8c +-----END PRIVATE KEY----- diff --git a/testdata/key2.pem b/testdata/key2.pem new file mode 100644 index 000000000..1c6deb284 --- /dev/null +++ b/testdata/key2.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+6bvLyg+VlYT/ +SU7lXJc2Dmdtehe7C/yckNpl+Zjj/8nkNRYfqJ2g8iwHASOWO7+Vvdg91Ti6eC+O +Mjg54iSmd2rZQ6734I+hSo3C/l4alkOArjDumQPddynjMiOaB67uS67+vyBwbQq1 +0BxOgJOhY/wQnLBJhOUhWh+UrjnL1LsET6qQicLwTzt/1cWzXmYSI8gTs0um+7r5 +WCB/Raf++KhboCtX3zEj6CV164ucEV65y2vQRIa7PQH+znCHGxbTnnZdielbJPc0 +S2hVtL2cRqocKCdEYKY3co27Hgvt/M7byETyLbmxbMK1r4f4icr9dyqKeY6aT+Px +Z43nggYdAgMBAAECggEAKUNxsLFivu0LSvY4VEC3+hoQ5sut12LW3aw9WC8jiZwe +sfF7b6pNL51IQNdRLsaJOT9IPs0YLs2NUcmu92vWihhjgsQrTC5APRdVHqFGC68Q +tf5wWxG9kR+RcSbEJSWl/KFlGHCM/V/EIdnyVFFcF1T6BUkonStZLuVA0Cz8Fv7r +MO2mtmwtOcTAQaUqHSxdcq9ASmuh3QA9tUmn11oCjHwVbKrFrKNQW20vSn71pk4Z +UiXnppRi7tk1t+PJb4VIEdUTw3JYwtp/HRgnfQ8VFDxWxWDPL4hHkBU270i7oIr3 ++DR0+A1eylCuKXO5Il75uMYs8mKjFyGCLFWoGWtW7QKBgQDlAva0FYrbDUFmdUjz +MCOVfOoPkaQB1bd9CK21b8HxGrcXbXwJzs/n2RV4GHZgodSADN2+fKvslpJofD3z +Cnim60MJ8WcxB8Ez+Jvfktap8Kox0qQEZPpIN9hQxBxcXaLL+9TcWvkYJwCDjy+7 +LmfHEh9puECnxw5YAlHAreRCowKBgQDVaWB80Ehp9Q4McescrUtbsA0CYI3mkZgx +6YU44/+DqiQStwQLttaOXvI4HXUqTdQgi8wMIJN/EOkMeAA1tuyULXJK9dkEB1pm +zHF65USCr29g2hr/PsLckryKMtnyDGRW7YimXz8P03t5LfJ/M6Ovc7QJm6HSwCkS +FyxrvU3gPwKBgCcWlGk0bBjrcEg+qI7pnok7Yu/5Wdb+VW0/9/ZJ9v5iIvIau9so +s4/NG7793easeIrKp2aF/QpKwP6YhjJfjSxgZ3bg/039Ftr6ChDlDULAUyxh2aDu +Y1HERmWys2yIhuruNuzNkkqvDYVnASyfxRLTYw02Z8K7VRVsf+u1QoqlAoGBAJ6i +HNnKTPmN8apoh3aijhCSdakdsn0AHpyDU8btG4J4VyYeKoC2oRflFbGGnBAdGCA1 +KjCdimX6YPEmxiknVwXyHjIAOxdWi+k78OKER3/I/kaE+Wpf8aLZ5BHqKL1WXsOK +/3eD9zFBZ1e1Qrsw3GxP2jUGHay1sBHFbfyME7YrAoGAXaa9GiSi/HjOgOS8nzGf +OzSl+I/fm651QjQ0tRVjVH777P3ZYyjdZNFYDb0x+fsvqTbkfkVtgeWuyoAVIwqy +BtWyvTYutjwwUi53oIpRBe3qhIWIQUZgyAwXVis8FChSv3aaCePij+Bwju6laIfj ++K5StCn2WaOE1d1akqF6C40= +-----END PRIVATE KEY-----