helmrepo: adopt Kubernetes TLS secrets for `.spec.certSecretRef`
Adopt Kubernetes TLS secrets API to check for TLS data in the Secret referred to by `.spec.certSecretRef`, i.e. check for keys `tls.crt` and `tls.key` for the certificate and private key. Use `ca.crt` for the CA certificate. Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
This commit is contained in:
parent
de31a124f7
commit
4bd6bcc9e9
|
@ -56,10 +56,21 @@ type HelmRepositorySpec struct {
|
||||||
// +optional
|
// +optional
|
||||||
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
|
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
|
||||||
|
|
||||||
// CertSecretRef specifies the Secret containing the TLS authentication
|
// CertSecretRef can be given the name of a Secret containing
|
||||||
// data. The secret must contain a 'certFile' and 'keyFile', and/or 'caFile'
|
// either or both of
|
||||||
// fields. It takes precedence over the values specified in the Secret
|
//
|
||||||
// referred to by `.spec.secretRef`.
|
// - a PEM-encoded client certificate (`tls.crt`) and private
|
||||||
|
// key (`tls.key`);
|
||||||
|
// - a PEM-encoded CA certificate (`ca.crt`)
|
||||||
|
//
|
||||||
|
// and whichever are supplied, will be used for connecting to the
|
||||||
|
// registry. The client cert and key are useful if you are
|
||||||
|
// authenticating with a certificate; the CA cert is useful if
|
||||||
|
// you are using a self-signed server certificate. The Secret must
|
||||||
|
// be of type `Opaque` or `kubernetes.io/tls`.
|
||||||
|
//
|
||||||
|
// It takes precedence over the values specified in the Secret referred
|
||||||
|
// to by `.spec.secretRef`.
|
||||||
// +optional
|
// +optional
|
||||||
CertSecretRef *meta.LocalObjectReference `json:"certSecretRef,omitempty"`
|
CertSecretRef *meta.LocalObjectReference `json:"certSecretRef,omitempty"`
|
||||||
|
|
||||||
|
|
|
@ -297,10 +297,15 @@ spec:
|
||||||
- namespaceSelectors
|
- namespaceSelectors
|
||||||
type: object
|
type: object
|
||||||
certSecretRef:
|
certSecretRef:
|
||||||
description: CertSecretRef specifies the Secret containing the TLS
|
description: "CertSecretRef can be given the name of a Secret containing
|
||||||
authentication data. The secret must contain a 'certFile' and 'keyFile',
|
either or both of \n - a PEM-encoded client certificate (`tls.crt`)
|
||||||
and/or 'caFile' fields. It takes precedence over the values specified
|
and private key (`tls.key`); - a PEM-encoded CA certificate (`ca.crt`)
|
||||||
in the Secret referred to by `.spec.secretRef`.
|
\n and whichever are supplied, will be used for connecting to the
|
||||||
|
registry. The client cert and key are useful if you are authenticating
|
||||||
|
with a certificate; the CA cert is useful if you are using a self-signed
|
||||||
|
server certificate. The Secret must be of type `Opaque` or `kubernetes.io/tls`.
|
||||||
|
\n It takes precedence over the values specified in the Secret referred
|
||||||
|
to by `.spec.secretRef`."
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
description: Name of the referent.
|
description: Name of the referent.
|
||||||
|
|
|
@ -811,10 +811,20 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<em>(Optional)</em>
|
<em>(Optional)</em>
|
||||||
<p>CertSecretRef specifies the Secret containing the TLS authentication
|
<p>CertSecretRef can be given the name of a Secret containing
|
||||||
data. The secret must contain a ‘certFile’ and ‘keyFile’, and/or ‘caFile’
|
either or both of</p>
|
||||||
fields. It takes precedence over the values specified in the Secret
|
<ul>
|
||||||
referred to by <code>.spec.secretRef</code>.</p>
|
<li>a PEM-encoded client certificate (<code>tls.crt</code>) and private
|
||||||
|
key (<code>tls.key</code>);</li>
|
||||||
|
<li>a PEM-encoded CA certificate (<code>ca.crt</code>)</li>
|
||||||
|
</ul>
|
||||||
|
<p>and whichever are supplied, will be used for connecting to the
|
||||||
|
registry. The client cert and key are useful if you are
|
||||||
|
authenticating with a certificate; the CA cert is useful if
|
||||||
|
you are using a self-signed server certificate. The Secret must
|
||||||
|
be of type <code>Opaque</code> or <code>kubernetes.io/tls</code>.</p>
|
||||||
|
<p>It takes precedence over the values specified in the Secret referred
|
||||||
|
to by <code>.spec.secretRef</code>.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -2503,10 +2513,20 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<em>(Optional)</em>
|
<em>(Optional)</em>
|
||||||
<p>CertSecretRef specifies the Secret containing the TLS authentication
|
<p>CertSecretRef can be given the name of a Secret containing
|
||||||
data. The secret must contain a ‘certFile’ and ‘keyFile’, and/or ‘caFile’
|
either or both of</p>
|
||||||
fields. It takes precedence over the values specified in the Secret
|
<ul>
|
||||||
referred to by <code>.spec.secretRef</code>.</p>
|
<li>a PEM-encoded client certificate (<code>tls.crt</code>) and private
|
||||||
|
key (<code>tls.key</code>);</li>
|
||||||
|
<li>a PEM-encoded CA certificate (<code>ca.crt</code>)</li>
|
||||||
|
</ul>
|
||||||
|
<p>and whichever are supplied, will be used for connecting to the
|
||||||
|
registry. The client cert and key are useful if you are
|
||||||
|
authenticating with a certificate; the CA cert is useful if
|
||||||
|
you are using a self-signed server certificate. The Secret must
|
||||||
|
be of type <code>Opaque</code> or <code>kubernetes.io/tls</code>.</p>
|
||||||
|
<p>It takes precedence over the values specified in the Secret referred
|
||||||
|
to by <code>.spec.secretRef</code>.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -467,32 +467,33 @@ flux create secret oci ghcr-auth \
|
||||||
--password=${GITHUB_PAT}
|
--password=${GITHUB_PAT}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** Support for specifying TLS authentication data using this API has been
|
**Warning:** Support for specifying TLS authentication data using this API has been
|
||||||
deprecated. Please use [`.spec.certSecretRef`](#cert-secret-reference) instead.
|
deprecated. Please use [`.spec.certSecretRef`](#cert-secret-reference) instead.
|
||||||
If the controller uses the secret specfied by this field to configure TLS, then
|
If the controller uses the secret specfied by this field to configure TLS, then
|
||||||
a deprecation warning will be logged.
|
a deprecation warning will be logged.
|
||||||
|
|
||||||
### Cert secret reference
|
### Cert secret reference
|
||||||
|
|
||||||
`.spec.certSecretRef.name` is an optional field to specify a secret containing TLS
|
`.spec.certSecretRef.name` is an optional field to specify a secret containing
|
||||||
certificate data. The secret can contain the following keys:
|
TLS certificate data. The secret can contain the following keys:
|
||||||
|
|
||||||
* `certFile` and `keyFile`, to specify the client certificate and private key used for
|
* `tls.crt` and `tls.key`, to specify the client certificate and private key used
|
||||||
TLS client authentication. These must be used in conjunction, i.e. specifying one without
|
for TLS client authentication. These must be used in conjunction, i.e.
|
||||||
the other will lead to an error.
|
specifying one without the other will lead to an error.
|
||||||
* `caFile`, to specify the CA certificate used to verify the server, which is required
|
* `ca.crt`, to specify the CA certificate used to verify the server, which is
|
||||||
if the server is using a self-signed certificate.
|
required if the server is using a self-signed certificate.
|
||||||
|
|
||||||
If the server is using a self-signed certificate and has TLS client authentication enabled,
|
If the server is using a self-signed certificate and has TLS client
|
||||||
all three values are required.
|
authentication enabled, all three values are required.
|
||||||
|
|
||||||
All the files in the secret are expected to be [PEM-encoded][pem-encoding]. Assuming you have
|
The Secret should be of type `Opaque` or `kubernetes.io/tls`. All the files in
|
||||||
three files; `client.key`, `client.crt` and `ca.crt` for the client private key, client
|
the Secret are expected to be [PEM-encoded][pem-encoding]. Assuming you have
|
||||||
certificate and the CA certificate respectively, you can generate the required secret using
|
three files; `client.key`, `client.crt` and `ca.crt` for the client private key,
|
||||||
the `flux creat secret helm` command:
|
client certificate and the CA certificate respectively, you can generate the
|
||||||
|
required Secret using the `flux create secret tls` command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
flux create secret helm tls --key-file=client.key --cert-file=client.crt --ca-file=ca.crt
|
flux create secret tls --tls-key-file=client.key --tls-crt-file=client.crt --ca-crt-file=ca.crt
|
||||||
```
|
```
|
||||||
|
|
||||||
Example usage:
|
Example usage:
|
||||||
|
@ -515,11 +516,12 @@ kind: Secret
|
||||||
metadata:
|
metadata:
|
||||||
name: example-tls
|
name: example-tls
|
||||||
namespace: default
|
namespace: default
|
||||||
|
type: kubernetes.io/tls # or Opaque
|
||||||
data:
|
data:
|
||||||
certFile: <BASE64>
|
tls.crt: <BASE64>
|
||||||
keyFile: <BASE64>
|
tls.key: <BASE64>
|
||||||
# NOTE: Can be supplied without the above values
|
# NOTE: Can be supplied without the above values
|
||||||
caFile: <BASE64>
|
ca.crt: <BASE64>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pass credentials
|
### Pass credentials
|
||||||
|
|
|
@ -2248,7 +2248,7 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
|
||||||
registryOpts registryOptions
|
registryOpts registryOptions
|
||||||
secretOpts secretOptions
|
secretOpts secretOptions
|
||||||
secret *corev1.Secret
|
secret *corev1.Secret
|
||||||
certsecret *corev1.Secret
|
certSecret *corev1.Secret
|
||||||
insecure bool
|
insecure bool
|
||||||
provider string
|
provider string
|
||||||
providerImg string
|
providerImg string
|
||||||
|
@ -2363,16 +2363,16 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
|
||||||
Type: corev1.SecretTypeDockerConfigJson,
|
Type: corev1.SecretTypeDockerConfigJson,
|
||||||
Data: map[string][]byte{},
|
Data: map[string][]byte{},
|
||||||
},
|
},
|
||||||
certsecret: &corev1.Secret{
|
certSecret: &corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "certs-secretref",
|
Name: "certs-secretref",
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"caFile": []byte("invalid caFile"),
|
"ca.crt": []byte("invalid caFile"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
*conditions.TrueCondition(sourcev1.FetchFailedCondition, "Unknown", "unknown build error: failed to construct Helm client's TLS config: cannot append certificate into certificate pool: invalid caFile"),
|
*conditions.TrueCondition(sourcev1.FetchFailedCondition, "Unknown", "unknown build error: failed to construct Helm client's TLS config: cannot append certificate into certificate pool: invalid CA certificate"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2393,14 +2393,14 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
|
||||||
Type: corev1.SecretTypeDockerConfigJson,
|
Type: corev1.SecretTypeDockerConfigJson,
|
||||||
Data: map[string][]byte{},
|
Data: map[string][]byte{},
|
||||||
},
|
},
|
||||||
certsecret: &corev1.Secret{
|
certSecret: &corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "certs-secretref",
|
Name: "certs-secretref",
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"caFile": tlsCA,
|
"ca.crt": tlsCA,
|
||||||
"certFile": clientPublicKey,
|
"tls.crt": clientPublicKey,
|
||||||
"keyFile": clientPrivateKey,
|
"tls.key": clientPrivateKey,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
|
@ -2472,11 +2472,11 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
|
||||||
clientBuilder.WithObjects(tt.secret)
|
clientBuilder.WithObjects(tt.secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tt.certsecret != nil {
|
if tt.certSecret != nil {
|
||||||
repo.Spec.CertSecretRef = &meta.LocalObjectReference{
|
repo.Spec.CertSecretRef = &meta.LocalObjectReference{
|
||||||
Name: tt.certsecret.Name,
|
Name: tt.certSecret.Name,
|
||||||
}
|
}
|
||||||
clientBuilder.WithObjects(tt.certsecret)
|
clientBuilder.WithObjects(tt.certSecret)
|
||||||
}
|
}
|
||||||
|
|
||||||
clientBuilder.WithObjects(repo)
|
clientBuilder.WithObjects(repo)
|
||||||
|
|
|
@ -325,12 +325,12 @@ func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) {
|
||||||
Name: "certs-secretref",
|
Name: "certs-secretref",
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"caFile": []byte("invalid caFile"),
|
"ca.crt": []byte("invalid caFile"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingWithRetryReason, "processing object: new generation 0 -> 1"),
|
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingWithRetryReason, "processing object: new generation 0 -> 1"),
|
||||||
*conditions.FalseCondition(meta.ReadyCondition, sourcev1.AuthenticationFailedReason, "cannot append certificate into certificate pool: invalid caFile"),
|
*conditions.FalseCondition(meta.ReadyCondition, sourcev1.AuthenticationFailedReason, "cannot append certificate into certificate pool: invalid CA certificate"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -356,9 +356,9 @@ func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) {
|
||||||
Name: "certs-secretref",
|
Name: "certs-secretref",
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"caFile": tlsCA,
|
"ca.crt": tlsCA,
|
||||||
"certFile": clientPublicKey,
|
"tls.crt": clientPublicKey,
|
||||||
"keyFile": clientPrivateKey,
|
"tls.key": clientPrivateKey,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
|
|
|
@ -56,6 +56,7 @@ import (
|
||||||
"github.com/fluxcd/source-controller/internal/helm/repository"
|
"github.com/fluxcd/source-controller/internal/helm/repository"
|
||||||
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
|
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
|
||||||
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
|
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
|
||||||
|
stls "github.com/fluxcd/source-controller/internal/tls"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHelmRepositoryReconciler_deleteBeforeFinalizer(t *testing.T) {
|
func TestHelmRepositoryReconciler_deleteBeforeFinalizer(t *testing.T) {
|
||||||
|
@ -434,7 +435,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
|
||||||
Name: "ca-file",
|
Name: "ca-file",
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"caFile": tlsCA,
|
"ca.crt": tlsCA,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev digest.Digest) {
|
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev digest.Digest) {
|
||||||
|
@ -445,6 +446,66 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
|
||||||
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new index revision"),
|
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new index revision"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "HTTPS with certSecretRef makes ArtifactOutdated=True",
|
||||||
|
protocol: "https",
|
||||||
|
server: options{
|
||||||
|
publicKey: tlsPublicKey,
|
||||||
|
privateKey: tlsPrivateKey,
|
||||||
|
ca: tlsCA,
|
||||||
|
},
|
||||||
|
secret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "ca-file",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"ca.crt": tlsCA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev digest.Digest) {
|
||||||
|
obj.Spec.CertSecretRef = &meta.LocalObjectReference{Name: "ca-file"}
|
||||||
|
},
|
||||||
|
want: sreconcile.ResultSuccess,
|
||||||
|
assertConditions: []metav1.Condition{
|
||||||
|
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new index revision"),
|
||||||
|
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new index revision"),
|
||||||
|
},
|
||||||
|
afterFunc: func(t *WithT, obj *helmv1.HelmRepository, artifact sourcev1.Artifact, chartRepo *repository.ChartRepository) {
|
||||||
|
t.Expect(chartRepo.Path).ToNot(BeEmpty())
|
||||||
|
t.Expect(chartRepo.Index).ToNot(BeNil())
|
||||||
|
t.Expect(artifact.Revision).ToNot(BeEmpty())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HTTPS with secretRef and caFile key makes ArtifactOutdated=True",
|
||||||
|
protocol: "https",
|
||||||
|
server: options{
|
||||||
|
publicKey: tlsPublicKey,
|
||||||
|
privateKey: tlsPrivateKey,
|
||||||
|
ca: tlsCA,
|
||||||
|
},
|
||||||
|
secret: &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "ca-file",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"caFile": tlsCA,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev digest.Digest) {
|
||||||
|
obj.Spec.SecretRef = &meta.LocalObjectReference{Name: "ca-file"}
|
||||||
|
},
|
||||||
|
want: sreconcile.ResultSuccess,
|
||||||
|
assertConditions: []metav1.Condition{
|
||||||
|
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "building artifact: new index revision"),
|
||||||
|
*conditions.UnknownCondition(meta.ReadyCondition, meta.ProgressingReason, "building artifact: new index revision"),
|
||||||
|
},
|
||||||
|
afterFunc: func(t *WithT, obj *helmv1.HelmRepository, artifact sourcev1.Artifact, chartRepo *repository.ChartRepository) {
|
||||||
|
t.Expect(chartRepo.Path).ToNot(BeEmpty())
|
||||||
|
t.Expect(chartRepo.Index).ToNot(BeNil())
|
||||||
|
t.Expect(artifact.Revision).ToNot(BeEmpty())
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "HTTP without secretRef makes ArtifactOutdated=True",
|
name: "HTTP without secretRef makes ArtifactOutdated=True",
|
||||||
protocol: "http",
|
protocol: "http",
|
||||||
|
@ -502,7 +563,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
|
||||||
Name: "invalid-ca",
|
Name: "invalid-ca",
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"caFile": []byte("invalid"),
|
"ca.crt": []byte("invalid"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev digest.Digest) {
|
beforeFunc: func(t *WithT, obj *helmv1.HelmRepository, rev digest.Digest) {
|
||||||
|
@ -512,7 +573,7 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
|
||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
assertConditions: []metav1.Condition{
|
assertConditions: []metav1.Condition{
|
||||||
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "cannot append certificate into certificate pool: invalid caFile"),
|
*conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, "cannot append certificate into certificate pool: invalid CA certificate"),
|
||||||
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
|
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingReason, "foo"),
|
||||||
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
|
*conditions.UnknownCondition(meta.ReadyCondition, "foo", "bar"),
|
||||||
},
|
},
|
||||||
|
@ -769,10 +830,16 @@ func TestHelmRepositoryReconciler_reconcileSource(t *testing.T) {
|
||||||
if tt.url != "" {
|
if tt.url != "" {
|
||||||
repoURL = tt.url
|
repoURL = tt.url
|
||||||
}
|
}
|
||||||
tlsConf, _, serr = getter.TLSClientConfigFromSecret(*secret, repoURL)
|
tlsConf, _, serr = stls.KubeTLSClientConfigFromSecret(*secret, repoURL)
|
||||||
if serr != nil {
|
if serr != nil {
|
||||||
validSecret = false
|
validSecret = false
|
||||||
}
|
}
|
||||||
|
if tlsConf == nil {
|
||||||
|
tlsConf, _, serr = stls.TLSClientConfigFromSecret(*secret, repoURL)
|
||||||
|
if serr != nil {
|
||||||
|
validSecret = false
|
||||||
|
}
|
||||||
|
}
|
||||||
newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, tlsConf, getterOpts...)
|
newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, tlsConf, getterOpts...)
|
||||||
} else {
|
} else {
|
||||||
newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, nil)
|
newChartRepo, err = repository.NewChartRepository(obj.Spec.URL, "", testGetters, nil)
|
||||||
|
|
|
@ -19,10 +19,8 @@ package getter
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
|
@ -37,6 +35,7 @@ import (
|
||||||
helmv1 "github.com/fluxcd/source-controller/api/v1beta2"
|
helmv1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||||
"github.com/fluxcd/source-controller/internal/helm/registry"
|
"github.com/fluxcd/source-controller/internal/helm/registry"
|
||||||
soci "github.com/fluxcd/source-controller/internal/oci"
|
soci "github.com/fluxcd/source-controller/internal/oci"
|
||||||
|
stls "github.com/fluxcd/source-controller/internal/tls"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -47,16 +46,6 @@ const (
|
||||||
|
|
||||||
var ErrDeprecatedTLSConfig = errors.New("TLS configured in a deprecated manner")
|
var ErrDeprecatedTLSConfig = errors.New("TLS configured in a deprecated manner")
|
||||||
|
|
||||||
// TLSBytes contains the bytes of the TLS files.
|
|
||||||
type TLSBytes struct {
|
|
||||||
// CertBytes is the bytes of the certificate file.
|
|
||||||
CertBytes []byte
|
|
||||||
// KeyBytes is the bytes of the key file.
|
|
||||||
KeyBytes []byte
|
|
||||||
// CABytes is the bytes of the CA file.
|
|
||||||
CABytes []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClientOpts contains the various options to use while constructing
|
// ClientOpts contains the various options to use while constructing
|
||||||
// a Helm repository client.
|
// a Helm repository client.
|
||||||
type ClientOpts struct {
|
type ClientOpts struct {
|
||||||
|
@ -91,7 +80,7 @@ func GetClientOpts(ctx context.Context, c client.Client, obj *helmv1.HelmReposit
|
||||||
|
|
||||||
var (
|
var (
|
||||||
certSecret *corev1.Secret
|
certSecret *corev1.Secret
|
||||||
tlsBytes *TLSBytes
|
tlsBytes *stls.TLSBytes
|
||||||
certFile string
|
certFile string
|
||||||
keyFile string
|
keyFile string
|
||||||
caFile string
|
caFile string
|
||||||
|
@ -105,7 +94,7 @@ func GetClientOpts(ctx context.Context, c client.Client, obj *helmv1.HelmReposit
|
||||||
return nil, "", fmt.Errorf("failed to get TLS authentication secret '%s/%s': %w", obj.GetNamespace(), obj.Spec.CertSecretRef.Name, err)
|
return nil, "", fmt.Errorf("failed to get TLS authentication secret '%s/%s': %w", obj.GetNamespace(), obj.Spec.CertSecretRef.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
hrOpts.TlsConfig, tlsBytes, err = TLSClientConfigFromSecret(*certSecret, url)
|
hrOpts.TlsConfig, tlsBytes, err = stls.KubeTLSClientConfigFromSecret(*certSecret, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("failed to construct Helm client's TLS config: %w", err)
|
return nil, "", fmt.Errorf("failed to construct Helm client's TLS config: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -128,8 +117,8 @@ func GetClientOpts(ctx context.Context, c client.Client, obj *helmv1.HelmReposit
|
||||||
|
|
||||||
// If the TLS config is nil, i.e. one couldn't be constructed using `.spec.certSecretRef`
|
// If the TLS config is nil, i.e. one couldn't be constructed using `.spec.certSecretRef`
|
||||||
// then try to use `.spec.secretRef`.
|
// then try to use `.spec.secretRef`.
|
||||||
if hrOpts.TlsConfig == nil {
|
if hrOpts.TlsConfig == nil && !ociRepo {
|
||||||
hrOpts.TlsConfig, tlsBytes, err = TLSClientConfigFromSecret(*authSecret, url)
|
hrOpts.TlsConfig, tlsBytes, err = stls.TLSClientConfigFromSecret(*authSecret, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("failed to construct Helm client's TLS config: %w", err)
|
return nil, "", fmt.Errorf("failed to construct Helm client's TLS config: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -162,7 +151,7 @@ func GetClientOpts(ctx context.Context, c client.Client, obj *helmv1.HelmReposit
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("cannot create temporary directory: %w", err)
|
return nil, "", fmt.Errorf("cannot create temporary directory: %w", err)
|
||||||
}
|
}
|
||||||
certFile, keyFile, caFile, err = StoreTLSCertificateFiles(tlsBytes, dir)
|
certFile, keyFile, caFile, err = storeTLSCertificateFiles(tlsBytes, dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("cannot write certs files to path: %w", err)
|
return nil, "", fmt.Errorf("cannot write certs files to path: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -198,60 +187,8 @@ func fetchSecret(ctx context.Context, c client.Client, name, namespace string) (
|
||||||
return &secret, nil
|
return &secret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TLSClientConfigFromSecret attempts to construct a TLS client config
|
// storeTLSCertificateFiles writes the certs files to the given path and returns the files paths.
|
||||||
// for the given v1.Secret. It returns the TLS client config or an error.
|
func storeTLSCertificateFiles(tlsBytes *stls.TLSBytes, path string) (string, string, string, error) {
|
||||||
//
|
|
||||||
// Secrets with no certFile, keyFile, AND caFile are ignored, if only a
|
|
||||||
// certBytes OR keyBytes is defined it returns an error.
|
|
||||||
func TLSClientConfigFromSecret(secret corev1.Secret, repositoryUrl string) (*tls.Config, *TLSBytes, error) {
|
|
||||||
certBytes, keyBytes, caBytes := secret.Data["certFile"], secret.Data["keyFile"], secret.Data["caFile"]
|
|
||||||
switch {
|
|
||||||
case len(certBytes)+len(keyBytes)+len(caBytes) == 0:
|
|
||||||
return nil, nil, nil
|
|
||||||
case (len(certBytes) > 0 && len(keyBytes) == 0) || (len(keyBytes) > 0 && len(certBytes) == 0):
|
|
||||||
return nil, nil, fmt.Errorf("invalid '%s' secret data: fields 'certFile' and 'keyFile' require each other's presence",
|
|
||||||
secret.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConf := &tls.Config{}
|
|
||||||
if len(certBytes) > 0 && len(keyBytes) > 0 {
|
|
||||||
cert, err := tls.X509KeyPair(certBytes, keyBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
tlsConf.Certificates = append(tlsConf.Certificates, cert)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(caBytes) > 0 {
|
|
||||||
cp, err := x509.SystemCertPool()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("cannot retrieve system certificate pool: %w", err)
|
|
||||||
}
|
|
||||||
if !cp.AppendCertsFromPEM(caBytes) {
|
|
||||||
return nil, nil, fmt.Errorf("cannot append certificate into certificate pool: invalid caFile")
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConf.RootCAs = cp
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConf.BuildNameToCertificate()
|
|
||||||
|
|
||||||
u, err := url.Parse(repositoryUrl)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("cannot parse repository URL: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConf.ServerName = u.Hostname()
|
|
||||||
|
|
||||||
return tlsConf, &TLSBytes{
|
|
||||||
CertBytes: certBytes,
|
|
||||||
KeyBytes: keyBytes,
|
|
||||||
CABytes: caBytes,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// StoreTLSCertificateFiles writes the certs files to the given path and returns the files paths.
|
|
||||||
func StoreTLSCertificateFiles(tlsBytes *TLSBytes, path string) (string, string, string, error) {
|
|
||||||
var (
|
var (
|
||||||
certFile string
|
certFile string
|
||||||
keyFile string
|
keyFile string
|
||||||
|
|
|
@ -18,11 +18,6 @@ package getter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"math/big"
|
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -58,7 +53,7 @@ func TestGetClientOpts(t *testing.T) {
|
||||||
Name: "ca-file",
|
Name: "ca-file",
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"caFile": tlsCA,
|
"ca.crt": tlsCA,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authSecret: &corev1.Secret{
|
authSecret: &corev1.Secret{
|
||||||
|
@ -160,42 +155,6 @@ func TestGetClientOpts(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_tlsClientConfigFromSecret(t *testing.T) {
|
|
||||||
tlsSecretFixture := validTlsSecret(t)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
secret corev1.Secret
|
|
||||||
modify func(secret *corev1.Secret)
|
|
||||||
wantErr bool
|
|
||||||
wantNil bool
|
|
||||||
}{
|
|
||||||
{"certFile, keyFile and caFile", tlsSecretFixture, nil, false, false},
|
|
||||||
{"without certFile", tlsSecretFixture, func(s *corev1.Secret) { delete(s.Data, "certFile") }, true, true},
|
|
||||||
{"without keyFile", tlsSecretFixture, func(s *corev1.Secret) { delete(s.Data, "keyFile") }, true, true},
|
|
||||||
{"without caFile", tlsSecretFixture, func(s *corev1.Secret) { delete(s.Data, "caFile") }, false, false},
|
|
||||||
{"empty", corev1.Secret{}, nil, false, true},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
secret := tt.secret.DeepCopy()
|
|
||||||
if tt.modify != nil {
|
|
||||||
tt.modify(secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
got, _, err := TLSClientConfigFromSecret(*secret, "")
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("TLSClientConfigFromSecret() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if tt.wantNil && got != nil {
|
|
||||||
t.Error("TLSClientConfigFromSecret() != nil")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetClientOpts_registryTLSLoginOption(t *testing.T) {
|
func TestGetClientOpts_registryTLSLoginOption(t *testing.T) {
|
||||||
tlsCA, err := os.ReadFile("../../controller/testdata/certs/ca.pem")
|
tlsCA, err := os.ReadFile("../../controller/testdata/certs/ca.pem")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -215,7 +174,7 @@ func TestGetClientOpts_registryTLSLoginOption(t *testing.T) {
|
||||||
Name: "ca-file",
|
Name: "ca-file",
|
||||||
},
|
},
|
||||||
Data: map[string][]byte{
|
Data: map[string][]byte{
|
||||||
"caFile": tlsCA,
|
"ca.crt": tlsCA,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authSecret: &corev1.Secret{
|
authSecret: &corev1.Secret{
|
||||||
|
@ -307,60 +266,3 @@ func TestGetClientOpts_registryTLSLoginOption(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// validTlsSecret creates a secret containing key pair and CA certificate that are
|
|
||||||
// valid from a syntax (minimum requirements) perspective.
|
|
||||||
func validTlsSecret(t *testing.T) corev1.Secret {
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Private key cannot be created.", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
certTemplate := x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(1337),
|
|
||||||
}
|
|
||||||
cert, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &key.PublicKey, key)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Certificate cannot be created.", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
ca := &x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(7331),
|
|
||||||
IsCA: true,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
|
||||||
}
|
|
||||||
|
|
||||||
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("CA private key cannot be created.", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("CA certificate cannot be created.", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
keyPem := pem.EncodeToMemory(&pem.Block{
|
|
||||||
Type: "RSA PRIVATE KEY",
|
|
||||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
|
||||||
})
|
|
||||||
|
|
||||||
certPem := pem.EncodeToMemory(&pem.Block{
|
|
||||||
Type: "CERTIFICATE",
|
|
||||||
Bytes: cert,
|
|
||||||
})
|
|
||||||
|
|
||||||
caPem := pem.EncodeToMemory(&pem.Block{
|
|
||||||
Type: "CERTIFICATE",
|
|
||||||
Bytes: caBytes,
|
|
||||||
})
|
|
||||||
|
|
||||||
return corev1.Secret{
|
|
||||||
Data: map[string][]byte{
|
|
||||||
"certFile": []byte(certPem),
|
|
||||||
"keyFile": []byte(keyPem),
|
|
||||||
"caFile": []byte(caPem),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
neturl "net/url"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CACrtKey = "ca.crt"
|
||||||
|
|
||||||
|
// TLSBytes contains the bytes of the TLS files.
|
||||||
|
type TLSBytes struct {
|
||||||
|
// CertBytes is the bytes of the certificate file.
|
||||||
|
CertBytes []byte
|
||||||
|
// KeyBytes is the bytes of the key file.
|
||||||
|
KeyBytes []byte
|
||||||
|
// CABytes is the bytes of the CA file.
|
||||||
|
CABytes []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// KubeTLSClientConfigFromSecret returns a TLS client config as a `tls.Config`
|
||||||
|
// object and in its bytes representation. The secret is expected to have the
|
||||||
|
// following keys:
|
||||||
|
// - tls.key, for the private key
|
||||||
|
// - tls.crt, for the certificate
|
||||||
|
// - ca.crt, for the CA certificate
|
||||||
|
//
|
||||||
|
// Secrets with no certificate, private key, AND CA cert are ignored. If only a
|
||||||
|
// certificate OR private key is found, an error is returned.
|
||||||
|
func KubeTLSClientConfigFromSecret(secret corev1.Secret, url string) (*tls.Config, *TLSBytes, error) {
|
||||||
|
return tlsClientConfigFromSecret(secret, url, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLSClientConfigFromSecret returns a TLS client config as a `tls.Config`
|
||||||
|
// object and in its bytes representation. The secret is expected to have the
|
||||||
|
// following keys:
|
||||||
|
// - keyFile, for the private key
|
||||||
|
// - certFile, for the certificate
|
||||||
|
// - caFile, for the CA certificate
|
||||||
|
//
|
||||||
|
// Secrets with no certificate, private key, AND CA cert are ignored. If only a
|
||||||
|
// certificate OR private key is found, an error is returned.
|
||||||
|
func TLSClientConfigFromSecret(secret corev1.Secret, url string) (*tls.Config, *TLSBytes, error) {
|
||||||
|
return tlsClientConfigFromSecret(secret, url, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tlsClientConfigFromSecret attempts to construct and return a TLS client
|
||||||
|
// config from the given Secret. If the Secret does not contain any TLS
|
||||||
|
// data, it returns nil.
|
||||||
|
//
|
||||||
|
// kubernetesTLSKeys is a boolean indicating whether to check the Secret
|
||||||
|
// for keys expected to be present in a Kubernetes TLS Secret. Based on its
|
||||||
|
// value, the Secret is checked for the following keys:
|
||||||
|
// - tls.key/keyFile for the private key
|
||||||
|
// - tls.crt/certFile for the certificate
|
||||||
|
// - ca.crt/caFile for the CA certificate
|
||||||
|
// The keys should adhere to a single convention, i.e. a Secret with tls.key
|
||||||
|
// and certFile is invalid.
|
||||||
|
func tlsClientConfigFromSecret(secret corev1.Secret, url string, kubernetesTLSKeys bool) (*tls.Config, *TLSBytes, error) {
|
||||||
|
// Only Secrets of type Opaque and TLS are allowed. We also allow Secrets with a blank
|
||||||
|
// type, to avoid having to specify the type of the Secret for every test case.
|
||||||
|
// Since a real Kubernetes Secret is of type Opaque by default, its safe to allow this.
|
||||||
|
switch secret.Type {
|
||||||
|
case corev1.SecretTypeOpaque, corev1.SecretTypeTLS, "":
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("cannot use secret '%s' to construct TLS config: invalid secret type: '%s'", secret.Name, secret.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
var certBytes, keyBytes, caBytes []byte
|
||||||
|
if kubernetesTLSKeys {
|
||||||
|
certBytes, keyBytes, caBytes = secret.Data[corev1.TLSCertKey], secret.Data[corev1.TLSPrivateKeyKey], secret.Data[CACrtKey]
|
||||||
|
} else {
|
||||||
|
certBytes, keyBytes, caBytes = secret.Data["certFile"], secret.Data["keyFile"], secret.Data["caFile"]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case len(certBytes)+len(keyBytes)+len(caBytes) == 0:
|
||||||
|
return nil, nil, nil
|
||||||
|
case (len(certBytes) > 0 && len(keyBytes) == 0) || (len(keyBytes) > 0 && len(certBytes) == 0):
|
||||||
|
return nil, nil, fmt.Errorf("invalid '%s' secret data: both certificate and private key need to be provided",
|
||||||
|
secret.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConf := &tls.Config{
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
if len(certBytes) > 0 && len(keyBytes) > 0 {
|
||||||
|
cert, err := tls.X509KeyPair(certBytes, keyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
tlsConf.Certificates = append(tlsConf.Certificates, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(caBytes) > 0 {
|
||||||
|
cp, err := x509.SystemCertPool()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("cannot retrieve system certificate pool: %w", err)
|
||||||
|
}
|
||||||
|
if !cp.AppendCertsFromPEM(caBytes) {
|
||||||
|
return nil, nil, fmt.Errorf("cannot append certificate into certificate pool: invalid CA certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConf.RootCAs = cp
|
||||||
|
}
|
||||||
|
|
||||||
|
if url != "" {
|
||||||
|
u, err := neturl.Parse(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("cannot parse repository URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConf.ServerName = u.Hostname()
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlsConf, &TLSBytes{
|
||||||
|
CertBytes: certBytes,
|
||||||
|
KeyBytes: keyBytes,
|
||||||
|
CABytes: caBytes,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,178 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_tlsClientConfigFromSecret(t *testing.T) {
|
||||||
|
kubernetesTlsSecretFixture := validTlsSecret(t, true)
|
||||||
|
tlsSecretFixture := validTlsSecret(t, false)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
secret corev1.Secret
|
||||||
|
modify func(secret *corev1.Secret)
|
||||||
|
tlsKeys bool
|
||||||
|
url string
|
||||||
|
wantErr bool
|
||||||
|
wantNil bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "tls.crt, tls.key and ca.crt",
|
||||||
|
secret: kubernetesTlsSecretFixture,
|
||||||
|
modify: nil,
|
||||||
|
tlsKeys: true,
|
||||||
|
url: "https://example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "certFile, keyFile and caFile",
|
||||||
|
secret: tlsSecretFixture,
|
||||||
|
modify: nil,
|
||||||
|
tlsKeys: false,
|
||||||
|
url: "https://example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "without tls.crt",
|
||||||
|
secret: kubernetesTlsSecretFixture,
|
||||||
|
modify: func(s *corev1.Secret) { delete(s.Data, "tls.crt") },
|
||||||
|
tlsKeys: true,
|
||||||
|
wantErr: true,
|
||||||
|
wantNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "without tls.key",
|
||||||
|
secret: kubernetesTlsSecretFixture,
|
||||||
|
modify: func(s *corev1.Secret) { delete(s.Data, "tls.key") },
|
||||||
|
tlsKeys: true,
|
||||||
|
wantErr: true,
|
||||||
|
wantNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "without ca.crt",
|
||||||
|
secret: kubernetesTlsSecretFixture,
|
||||||
|
modify: func(s *corev1.Secret) { delete(s.Data, "ca.crt") },
|
||||||
|
tlsKeys: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty secret",
|
||||||
|
secret: corev1.Secret{},
|
||||||
|
tlsKeys: true,
|
||||||
|
wantNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid secret type",
|
||||||
|
secret: corev1.Secret{Type: corev1.SecretTypeDockerConfigJson},
|
||||||
|
wantErr: true,
|
||||||
|
wantNil: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
secret := tt.secret.DeepCopy()
|
||||||
|
if tt.modify != nil {
|
||||||
|
tt.modify(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig, _, err := tlsClientConfigFromSecret(*secret, tt.url, tt.tlsKeys)
|
||||||
|
g.Expect(err != nil).To(Equal(tt.wantErr), fmt.Sprintf("expected error: %v, got: %v", tt.wantErr, err))
|
||||||
|
g.Expect(tlsConfig == nil).To(Equal(tt.wantNil))
|
||||||
|
if tt.url != "" {
|
||||||
|
u, _ := url.Parse(tt.url)
|
||||||
|
g.Expect(u.Hostname()).To(Equal(tlsConfig.ServerName))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validTlsSecret creates a secret containing key pair and CA certificate that are
|
||||||
|
// valid from a syntax (minimum requirements) perspective.
|
||||||
|
func validTlsSecret(t *testing.T, kubernetesTlsKeys bool) corev1.Secret {
|
||||||
|
t.Helper()
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Private key cannot be created.", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
certTemplate := x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(1337),
|
||||||
|
}
|
||||||
|
cert, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &key.PublicKey, key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Certificate cannot be created.", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
ca := &x509.Certificate{
|
||||||
|
SerialNumber: big.NewInt(7331),
|
||||||
|
IsCA: true,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||||
|
}
|
||||||
|
|
||||||
|
caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("CA private key cannot be created.", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("CA certificate cannot be created.", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPem := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||||
|
})
|
||||||
|
|
||||||
|
certPem := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: cert,
|
||||||
|
})
|
||||||
|
|
||||||
|
caPem := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: caBytes,
|
||||||
|
})
|
||||||
|
|
||||||
|
crtKey := corev1.TLSCertKey
|
||||||
|
pkKey := corev1.TLSPrivateKeyKey
|
||||||
|
caKey := CACrtKey
|
||||||
|
if !kubernetesTlsKeys {
|
||||||
|
crtKey = "certFile"
|
||||||
|
pkKey = "keyFile"
|
||||||
|
caKey = "caFile"
|
||||||
|
}
|
||||||
|
return corev1.Secret{
|
||||||
|
Data: map[string][]byte{
|
||||||
|
crtKey: []byte(certPem),
|
||||||
|
pkKey: []byte(keyPem),
|
||||||
|
caKey: []byte(caPem),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue