Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adopt Kubernetes style TLS Secret #597

Merged
merged 1 commit into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion api/v1beta2/provider_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,11 @@ type ProviderSpec struct {
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`

// CertSecretRef specifies the Secret containing
// a PEM-encoded CA certificate (`caFile`).
// a PEM-encoded CA certificate (in the `ca.crt` key).
// +optional
//
// Note: Support for the `caFile` key has
// been deprecated.
CertSecretRef *meta.LocalObjectReference `json:"certSecretRef,omitempty"`

// Suspend tells the controller to suspend subsequent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,9 @@ spec:
maxLength: 2048
type: string
certSecretRef:
description: CertSecretRef specifies the Secret containing a PEM-encoded
CA certificate (`caFile`).
description: "CertSecretRef specifies the Secret containing a PEM-encoded
CA certificate (in the `ca.crt` key). \n Note: Support for the `caFile`
key has been deprecated."
properties:
name:
description: Name of the referent.
Expand Down
8 changes: 6 additions & 2 deletions docs/api/v1beta2/notification.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,9 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
<td>
<em>(Optional)</em>
<p>CertSecretRef specifies the Secret containing
a PEM-encoded CA certificate (<code>caFile</code>).</p>
a PEM-encoded CA certificate (in the <code>ca.crt</code> key).</p>
<p>Note: Support for the <code>caFile</code> key has
been deprecated.</p>
</td>
</tr>
<tr>
Expand Down Expand Up @@ -964,7 +966,9 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
<td>
<em>(Optional)</em>
<p>CertSecretRef specifies the Secret containing
a PEM-encoded CA certificate (<code>caFile</code>).</p>
a PEM-encoded CA certificate (in the <code>ca.crt</code> key).</p>
<p>Note: Support for the <code>caFile</code> key has
been deprecated.</p>
</td>
</tr>
<tr>
Expand Down
10 changes: 8 additions & 2 deletions docs/spec/v1beta2/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -1095,11 +1095,12 @@ stringData:

`.spec.certSecretRef` is an optional field to specify a name reference to a
Secret in the same namespace as the Provider, containing the TLS CA certificate.
The secret must be of type `kubernetes.io/tls` or `Opaque`.

#### Example

To enable notification-controller to communicate with a provider API over HTTPS
using a self-signed TLS certificate, set the `caFile` like so:
using a self-signed TLS certificate, set the `ca.crt` like so:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we also need to add a deprecation notice for the caFile key like the one here: https://github.com/fluxcd/source-controller/blob/main/docs/spec/v1beta2/helmrepositories.md#secret-reference

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my bad, skipped it somehow but the same goes for the API docs


```yaml
---
Expand All @@ -1119,11 +1120,16 @@ kind: Secret
metadata:
name: my-ca-crt
namespace: default
type: kubernetes.io/tls # or Opaque
stringData:
caFile: |
ca.crt: |
<--- CA Key --->
```

**Warning:** Support for the `caFile` key has been
deprecated. If you have any Secrets using this key,
the controller will log a deprecation warning.

### HTTP/S proxy

`.spec.proxy` is an optional field to specify an HTTP/S proxy address.
Expand Down
2 changes: 1 addition & 1 deletion internal/controller/alert_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func (r *AlertReconciler) isProviderReady(ctx context.Context, alert *apiv1beta2
providerName := types.NamespacedName{Namespace: alert.Namespace, Name: alert.Spec.ProviderRef.Name}
if err := r.Get(ctx, providerName, provider); err != nil {
// log not found errors since they get filtered out
ctrl.LoggerFrom(ctx).Error(err, "failed to get provider %s", providerName.String())
ctrl.LoggerFrom(ctx).Error(err, "failed to get provider", "provider", providerName.String())
return fmt.Errorf("failed to get provider '%s': %w", providerName.String(), err)
}

Expand Down
16 changes: 14 additions & 2 deletions internal/controller/provider_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ func (r *ProviderReconciler) validateURLs(provider *apiv1beta2.Provider) error {
}

func (r *ProviderReconciler) validateCredentials(ctx context.Context, provider *apiv1beta2.Provider) error {
log := ctrl.LoggerFrom(ctx)

address := provider.Spec.Address
proxy := provider.Spec.Proxy
username := provider.Spec.Username
Expand Down Expand Up @@ -245,9 +247,19 @@ func (r *ProviderReconciler) validateCredentials(ctx context.Context, provider *
return fmt.Errorf("failed to read secret, error: %w", err)
}

caFile, ok := secret.Data["caFile"]
switch secret.Type {
case corev1.SecretTypeOpaque, corev1.SecretTypeTLS, "":
default:
return fmt.Errorf("cannot use secret '%s' to get TLS certificate: invalid secret type: '%s'", secret.Name, secret.Type)
}

caFile, ok := secret.Data["ca.crt"]
if !ok {
return fmt.Errorf("no caFile found in secret %s", provider.Spec.CertSecretRef.Name)
caFile, ok = secret.Data["caFile"]
if !ok {
return fmt.Errorf("no 'ca.crt' key found in secret '%s'", provider.Spec.CertSecretRef.Name)
}
log.Info("warning: specifying CA cert via 'caFile' is deprecated, please use 'ca.crt' instead")
}

certPool = x509.NewCertPool()
Expand Down
97 changes: 97 additions & 0 deletions internal/controller/provider_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package controller
import (
"context"
"fmt"
"os"
"testing"
"time"

Expand Down Expand Up @@ -237,3 +238,99 @@ func TestProviderReconciler_Reconcile(t *testing.T) {
}, timeout, time.Second).Should(BeTrue())
})
}

func TestProviderReconciler_Reconcile_cacert(t *testing.T) {
g := NewWithT(t)
namespaceName := "provider-" + randStringRunes(5)
secretName := "ca-secret-" + randStringRunes(5)

caCrt, err := os.ReadFile("./testdata/certs/ca.pem")
g.Expect(err).To(Not(HaveOccurred()))

g.Expect(createNamespace(namespaceName)).NotTo(HaveOccurred(), "failed to create test namespace")

providerKey := types.NamespacedName{
Name: fmt.Sprintf("provider-%s", randStringRunes(5)),
Namespace: namespaceName,
}

provider := &apiv1beta2.Provider{
ObjectMeta: metav1.ObjectMeta{
Name: providerKey.Name,
Namespace: providerKey.Namespace,
},
Spec: apiv1beta2.ProviderSpec{
Type: "generic",
Address: "https://webhook.internal",
CertSecretRef: &meta.LocalObjectReference{Name: secretName},
},
}
g.Expect(k8sClient.Create(context.Background(), provider)).To(Succeed())

certSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: providerKey.Namespace,
},
Data: map[string][]byte{
"caFile": []byte("invalid byte"),
"ca.crt": caCrt,
},
}
g.Expect(k8sClient.Create(context.Background(), certSecret)).To(Succeed())

r := &ProviderReconciler{
Client: k8sClient,
EventRecorder: record.NewFakeRecorder(32),
}

t.Run("uses `ca.crt` instead of deprecated `caFile`", func(t *testing.T) {
g := NewWithT(t)
_, err = r.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(provider)})
g.Expect(err).NotTo(HaveOccurred())
})

t.Run("works if only deprecated `caFile` is specified", func(t *testing.T) {
g := NewWithT(t)

clusterCertSecret := &corev1.Secret{}
g.Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(certSecret), clusterCertSecret)).To(Succeed())

patchHelper, err := patch.NewHelper(clusterCertSecret, k8sClient)
g.Expect(err).ToNot(HaveOccurred())
clusterCertSecret.Data = map[string][]byte{
"caFile": caCrt,
}
g.Expect(patchHelper.Patch(context.Background(), clusterCertSecret)).ToNot(HaveOccurred())

_, err = r.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(provider)})
g.Expect(err).NotTo(HaveOccurred())
})

t.Run("returns error with certSecretRef of the wrong type", func(t *testing.T) {
g := NewWithT(t)

dockerSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "docker-secret",
Namespace: providerKey.Namespace,
},
Type: corev1.DockerConfigJsonKey,
}
g.Expect(k8sClient.Create(context.Background(), dockerSecret)).To(Succeed())

clusterProvider := &apiv1beta2.Provider{}
g.Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(provider), clusterProvider)).To(Succeed())

patchHelper, err := patch.NewHelper(clusterProvider, k8sClient)
g.Expect(err).ToNot(HaveOccurred())
clusterProvider.Spec.CertSecretRef = &meta.LocalObjectReference{
Name: dockerSecret.Name,
}
g.Expect(patchHelper.Patch(context.Background(), clusterProvider)).ToNot(HaveOccurred())

_, err = r.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(provider)})
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring("invalid secret type"))
})
}
20 changes: 20 additions & 0 deletions internal/controller/testdata/certs/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2023 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.

all: ca-key.pem

ca-key.pem: ca-csr.json
cfssl gencert -initca ca-csr.json | cfssljson -bare ca –
ca.pem: ca-key.pem
ca.csr: ca-key.pem
13 changes: 13 additions & 0 deletions internal/controller/testdata/certs/ca-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"signing": {
"default": {
"expiry": "8760h"
},
"profiles": {
"kubernetes": {
"usages": ["signing", "key encipherment", "server auth", "client auth"],
"expiry": "8760h"
}
}
}
}
9 changes: 9 additions & 0 deletions internal/controller/testdata/certs/ca-csr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"CN": "example.com CA",
"hosts": [
"127.0.0.1",
"localhost",
"example.com",
"www.example.com"
]
}
5 changes: 5 additions & 0 deletions internal/controller/testdata/certs/ca-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIEY+R5pZqRQUvD2wrbL4HFl5IKFY84FEGHG5GHW1EojroAoGCCqGSM49
AwEHoUQDQgAEDqdWaSYf2JT7L90ywnuhz2BS4zhy+v5juPLpBI0Indo8mHpOw9m+
LCnMzN2WkcUYt+XLQwhaNNt0RBlfnXRzhw==
-----END EC PRIVATE KEY-----
9 changes: 9 additions & 0 deletions internal/controller/testdata/certs/ca.csr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBHzCBxgIBADAZMRcwFQYDVQQDEw5leGFtcGxlLmNvbSBDQTBZMBMGByqGSM49
AgEGCCqGSM49AwEHA0IABA6nVmkmH9iU+y/dMsJ7oc9gUuM4cvr+Y7jy6QSNCJ3a
PJh6TsPZviwpzMzdlpHFGLfly0MIWjTbdEQZX510c4egSzBJBgkqhkiG9w0BCQ4x
PDA6MDgGA1UdEQQxMC+CCWxvY2FsaG9zdIILZXhhbXBsZS5jb22CD3d3dy5leGFt
cGxlLmNvbYcEfwAAATAKBggqhkjOPQQDAgNIADBFAiEAl0UiOdxlEcuKrNAGh/Pv
CFxMyX5shaeAsdGvq/gyXckCIFlTwheOJZZVRRQl9b0l5LUVeJyIH6mnvitFGyQ7
NRk5
-----END CERTIFICATE REQUEST-----
11 changes: 11 additions & 0 deletions internal/controller/testdata/certs/ca.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-----BEGIN CERTIFICATE-----
MIIBhzCCAS2gAwIBAgIUXe+UdziaK3dAdwRgwpWa0XPkKLIwCgYIKoZIzj0EAwIw
GTEXMBUGA1UEAxMOZXhhbXBsZS5jb20gQ0EwHhcNMjMwODE2MTcwMDAwWhcNMjgw
ODE0MTcwMDAwWjAZMRcwFQYDVQQDEw5leGFtcGxlLmNvbSBDQTBZMBMGByqGSM49
AgEGCCqGSM49AwEHA0IABA6nVmkmH9iU+y/dMsJ7oc9gUuM4cvr+Y7jy6QSNCJ3a
PJh6TsPZviwpzMzdlpHFGLfly0MIWjTbdEQZX510c4ejUzBRMA4GA1UdDwEB/wQE
AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRSCe96Yb+o/q3kuqBp5IMs
uD9N/DAPBgNVHREECDAGhwR/AAABMAoGCCqGSM49BAMCA0gAMEUCIBw52JoEQ/0Z
Bnz4gXlXLXtVX0C4LvcNohdlwSRGHPlYAiEAnVqcT2kxBs2+E3SqJPU3DUM7ZFOO
n3zfiIVinQNlXPY=
-----END CERTIFICATE-----
20 changes: 16 additions & 4 deletions internal/server/event_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,16 +205,28 @@ func (s *EventServer) handleEvent() func(w http.ResponseWriter, r *http.Request)
continue
}

caFile, ok := secret.Data["caFile"]
if !ok {
alertLogger.Error(err, "failed to read secret key caFile")
switch secret.Type {
case corev1.SecretTypeOpaque, corev1.SecretTypeTLS, "":
default:
alertLogger.Error(nil, "cannot use secret '%s' to get TLS certificate: invalid secret type: '%s'",
secret.Name, secret.Type)
continue
}

caFile, ok := secret.Data["ca.crt"]
if !ok {
caFile, ok = secret.Data["caFile"]
if !ok {
alertLogger.Error(nil, "no 'ca.crt' key found in secret '%s'", provider.Spec.CertSecretRef.Name)
continue
}
alertLogger.Info("warning: specifying CA cert via 'caFile' is deprecated, please use 'ca.crt' instead")
}

certPool = x509.NewCertPool()
ok = certPool.AppendCertsFromPEM(caFile)
if !ok {
alertLogger.Error(err, "could not append to cert pool")
alertLogger.Error(nil, "could not append to cert pool")
continue
}
}
Expand Down