diff --git a/runtime/tls/doc.go b/runtime/tls/doc.go new file mode 100644 index 000000000..b31aa39e1 --- /dev/null +++ b/runtime/tls/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package tls contains helpers to convert Kubernetes secrets to TLS certificates. +package tls diff --git a/runtime/tls/testdata/keys.go b/runtime/tls/testdata/keys.go new file mode 100644 index 000000000..44f48fc66 --- /dev/null +++ b/runtime/tls/testdata/keys.go @@ -0,0 +1,51 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package testdata + +var ExampleCA = []byte(`-----BEGIN CERTIFICATE----- +MIIB7TCCAZKgAwIBAgIUB+17B8PU05wVTzRHLeG+S+ybZK4wCgYIKoZIzj0EAwIw +GTEXMBUGA1UEAxMOZXhhbXBsZS5jb20gQ0EwHhcNMjAwNDE3MDgxODAwWhcNMzAw +NDE1MDgxODAwWjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABPud6ARpa71VE3pUhmI4vBjR1YVYPIzrNT3ni7lwiGY0JnLR +yxRKt3xPphsQi/dzLaWM5cCFgX9Ju6RBkK3um86jgbowgbcwDgYDVR0PAQH/BAQD +AgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAA +MB0GA1UdDgQWBBTM8HS5EIlVMBYv/300jN8PEArUgDAfBgNVHSMEGDAWgBQGyUiU +1QEZiMAqjsnIYTwZ4yp5wzA4BgNVHREEMTAvgglsb2NhbGhvc3SCC2V4YW1wbGUu +Y29tgg93d3cuZXhhbXBsZS5jb22HBH8AAAEwCgYIKoZIzj0EAwIDSQAwRgIhAOgB +5W82FEgiTTOmsNRekkK5jUPbj4D4eHtb2/BI7ph4AiEA2AxHASIFBdv5b7Qf5prb +bdNmUCzAvVuCAKuMjg2OPrE= +-----END CERTIFICATE-----`) + +var ExampleCert = []byte(`-----BEGIN CERTIFICATE----- +MIIB7TCCAZKgAwIBAgIUB+17B8PU05wVTzRHLeG+S+ybZK4wCgYIKoZIzj0EAwIw +GTEXMBUGA1UEAxMOZXhhbXBsZS5jb20gQ0EwHhcNMjAwNDE3MDgxODAwWhcNMzAw +NDE1MDgxODAwWjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTBZMBMGByqGSM49AgEG +CCqGSM49AwEHA0IABPud6ARpa71VE3pUhmI4vBjR1YVYPIzrNT3ni7lwiGY0JnLR +yxRKt3xPphsQi/dzLaWM5cCFgX9Ju6RBkK3um86jgbowgbcwDgYDVR0PAQH/BAQD +AgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAA +MB0GA1UdDgQWBBTM8HS5EIlVMBYv/300jN8PEArUgDAfBgNVHSMEGDAWgBQGyUiU +1QEZiMAqjsnIYTwZ4yp5wzA4BgNVHREEMTAvgglsb2NhbGhvc3SCC2V4YW1wbGUu +Y29tgg93d3cuZXhhbXBsZS5jb22HBH8AAAEwCgYIKoZIzj0EAwIDSQAwRgIhAOgB +5W82FEgiTTOmsNRekkK5jUPbj4D4eHtb2/BI7ph4AiEA2AxHASIFBdv5b7Qf5prb +bdNmUCzAvVuCAKuMjg2OPrE= +-----END CERTIFICATE-----`) + +var ExampleKey = []byte(`-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIKQbEXV6nljOHMmPrWVWQ+JrAE5wsbE9iMhfY7wlJgXOoAoGCCqGSM49 +AwEHoUQDQgAE+53oBGlrvVUTelSGYji8GNHVhVg8jOs1PeeLuXCIZjQmctHLFEq3 +fE+mGxCL93MtpYzlwIWBf0m7pEGQre6bzg== +-----END EC PRIVATE KEY-----`) diff --git a/runtime/tls/tls.go b/runtime/tls/tls.go new file mode 100644 index 000000000..411d0c2fd --- /dev/null +++ b/runtime/tls/tls.go @@ -0,0 +1,81 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tls + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + + corev1 "k8s.io/api/core/v1" +) + +const ( + ClientCertIdentifier = "certFile" + ClientKeyIdentifier = "keyFile" + CACertIdentifier = "caFile" +) + +// ConfigFromSecret returns a TLS config created from the content of the secret. +// An error is returned if the secret does not contain a ClientCertIdentifier and ClientKeyIdentifier, or a +// CACertIdentifier. +func ConfigFromSecret(certSecret *corev1.Secret) (*tls.Config, error) { + validSecret := false + tlsConfig := &tls.Config{} + + clientCert, clientCertOk := certSecret.Data[ClientCertIdentifier] + clientKey, clientKeyOk := certSecret.Data[ClientKeyIdentifier] + if clientKeyOk != clientCertOk { + return nil, fmt.Errorf("found one of %s or %s, and expected both or neither", ClientCertIdentifier, ClientKeyIdentifier) + } + if clientCertOk && clientKeyOk { + validSecret = true + cert, err := tls.X509KeyPair(clientCert, clientKey) + if err != nil { + return nil, err + } + tlsConfig.Certificates = append(tlsConfig.Certificates, cert) + } + + if caCert, ok := certSecret.Data[CACertIdentifier]; ok { + validSecret = true + sysCerts, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + sysCerts.AppendCertsFromPEM(caCert) + tlsConfig.RootCAs = sysCerts + } + + if !validSecret { + return nil, fmt.Errorf("no %s and %s, or %s found in secret", ClientCertIdentifier, ClientKeyIdentifier, CACertIdentifier) + } + + return tlsConfig, nil +} + +// TransportFromSecret returns a HTTP transport with a TLS config created from the content of the secret. +// An error is returned if the secret does not contain a ClientCertIdentifier and ClientKeyIdentifier, or a +// CACertIdentifier. +func TransportFromSecret(certSecret *corev1.Secret) (*http.Transport, error) { + tlsConfig, err := ConfigFromSecret(certSecret) + if err != nil { + return nil, err + } + return &http.Transport{TLSClientConfig: tlsConfig}, nil +} diff --git a/runtime/tls/tls_test.go b/runtime/tls/tls_test.go new file mode 100644 index 000000000..361914d01 --- /dev/null +++ b/runtime/tls/tls_test.go @@ -0,0 +1,110 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tls + +import ( + "crypto/tls" + "testing" + + "github.com/fluxcd/pkg/runtime/tls/testdata" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" +) + +func TestCert_TlsConfigAll(t *testing.T) { + secret := &corev1.Secret{ + Data: map[string][]byte{ + CACertIdentifier: testdata.ExampleCA, + ClientCertIdentifier: testdata.ExampleCert, + ClientKeyIdentifier: testdata.ExampleKey, + }, + } + tlsConfig, err := ConfigFromSecret(secret) + require.NoError(t, err) + cert, err := tls.X509KeyPair(testdata.ExampleCert, testdata.ExampleKey) + require.NoError(t, err) + require.Equal(t, tlsConfig.Certificates[0], cert) +} + +func TestCert_TlsConfigNone(t *testing.T) { + secret := &corev1.Secret{ + Data: map[string][]byte{}, + } + tlsConfig, err := ConfigFromSecret(secret) + require.EqualError(t, err, "no certFile and keyFile, or caFile found in secret") + require.Nil(t, tlsConfig) +} + +func TestCert_TlsConfigOnlyCa(t *testing.T) { + secret := &corev1.Secret{ + Data: map[string][]byte{ + CACertIdentifier: testdata.ExampleCA, + }, + } + tlsConfig, err := ConfigFromSecret(secret) + require.NoError(t, err) + require.NotNil(t, tlsConfig) +} + +func TestCert_TlsConfigOnlyClient(t *testing.T) { + secret := &corev1.Secret{ + Data: map[string][]byte{ + ClientCertIdentifier: testdata.ExampleCert, + ClientKeyIdentifier: testdata.ExampleKey, + }, + } + tlsConfig, err := ConfigFromSecret(secret) + require.NoError(t, err) + require.NotNil(t, tlsConfig) +} + +func TestCert_TlsConfigMissingKey(t *testing.T) { + secret := &corev1.Secret{ + Data: map[string][]byte{ + CACertIdentifier: testdata.ExampleCA, + ClientCertIdentifier: testdata.ExampleCert, + }, + } + tlsConfig, err := ConfigFromSecret(secret) + require.EqualError(t, err, "found one of certFile or keyFile, and expected both or neither") + require.Nil(t, tlsConfig) +} + +func TestCert_TlsConfigMissingCert(t *testing.T) { + secret := &corev1.Secret{ + Data: map[string][]byte{ + CACertIdentifier: testdata.ExampleCA, + ClientKeyIdentifier: testdata.ExampleKey, + }, + } + tlsConfig, err := ConfigFromSecret(secret) + require.EqualError(t, err, "found one of certFile or keyFile, and expected both or neither") + require.Nil(t, tlsConfig) +} + +func TestCert_Transport(t *testing.T) { + secret := &corev1.Secret{ + Data: map[string][]byte{ + CACertIdentifier: testdata.ExampleCA, + ClientCertIdentifier: testdata.ExampleCert, + ClientKeyIdentifier: testdata.ExampleKey, + }, + } + transport, err := TransportFromSecret(secret) + require.NoError(t, err) + require.NotNil(t, transport.TLSClientConfig) +}