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

issue self signed certificate #316

Merged
merged 2 commits into from
Jun 21, 2024
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
22 changes: 11 additions & 11 deletions globals/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ package globals

import "fmt"

const ProjectName string = "idpbuilder"
const giteaResourceName string = "gitea"
const gitServerResourceName string = "gitserver"
const (
ProjectName string = "idpbuilder"

func GetProjectNamespace(name string) string {
return fmt.Sprintf("%s-%s", ProjectName, name)
}
NginxNamespace string = "ingress-nginx"

func GiteaResourceName() string {
return giteaResourceName
}
SelfSignedCertSecretName = "idpbuilder-cert"
SelfSignedCertCMName = "idpbuilder-cert"
SelfSignedCertCMKeyName = "ca.crt"
DefaultSANWildcard = "*.cnoe.localtest.me"
DefaultHostName = "cnoe.localtest.me"
)

func GitServerResourcename() string {
return gitServerResourceName
func GetProjectNamespace(name string) string {
return fmt.Sprintf("%s-%s", ProjectName, name)
}
12 changes: 12 additions & 0 deletions hack/argo-cd/argocd-tls-certs-cm.yaml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-tls-certs-cm
labels:
app.kubernetes.io/name: argocd-tls-certs-cm
app.kubernetes.io/part-of: argocd
data:
'gitea.cnoe.localtest.me': |
{{ .SelfSignedCert | indentNewLines 4 }}
'{{.Host}}': |
{{ .SelfSignedCert | indentNewLines 4 }}
cmoulliard marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions hack/argo-cd/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ patches:
- path: argocd-applicationset-controller.yaml
- path: argocd-repo-server.yaml
- path: argocd-redis.yaml
- path: argocd-tls-certs-cm.yaml.tmpl
1 change: 1 addition & 0 deletions hack/ingress-nginx/deployment-ingress-nginx.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ spec:
- --watch-ingress-without-class=true
- --publish-status-address=localhost
- --enable-ssl-passthrough
- --default-ssl-certificate=ingress-nginx/idpbuilder-cert
ports:
- containerPort: 80
hostPort: 80
Expand Down
7 changes: 7 additions & 0 deletions pkg/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ func (b *Build) Run(ctx context.Context, recreateCluster bool) error {
return err
}

setupLog.Info("Setting up TLS certificate")
cert, err := setupSelfSignedCertificate(ctx, setupLog, kubeClient, b.cfg)
if err != nil {
return err
}
b.cfg.SelfSignedCert = string(cert)

setupLog.V(1).Info("Running controllers")
if err := b.RunControllers(ctx, mgr, managerExit, dir); err != nil {
setupLog.Error(err, "Error running controllers")
Expand Down
204 changes: 204 additions & 0 deletions pkg/build/tls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package build

import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io"
"math/big"
"time"

"github.com/cnoe-io/idpbuilder/globals"
"github.com/cnoe-io/idpbuilder/pkg/k8s"
"github.com/cnoe-io/idpbuilder/pkg/util"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
certificateOrgName = "cnoe.io"
)

var (
certificateValidLength = time.Hour * 8766 // one year
)

func createIngressCertificateSecret(ctx context.Context, kubeClient client.Client, cert []byte) error {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: globals.SelfSignedCertCMName,
Namespace: corev1.NamespaceDefault,
},
Data: map[string][]byte{
globals.SelfSignedCertCMKeyName: cert,
},
}
err := kubeClient.Create(ctx, secret)
if err != nil {
if k8serrors.IsAlreadyExists(err) {
return nil
}
return fmt.Errorf("creating configmap for certificate: %w", err)
}
return nil
}

func getIngressCertificateAndKey(ctx context.Context, kubeClient client.Client, name, namespace string) ([]byte, []byte, error) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Type: corev1.SecretTypeTLS,
}

err := kubeClient.Get(ctx, client.ObjectKeyFromObject(secret), secret)
if err != nil {
return nil, nil, err
}
cert, ok := secret.Data[corev1.TLSCertKey]
if !ok {
return nil, nil, fmt.Errorf("key %s not found in secret %s", corev1.TLSCertKey, name)
}
privateKey, ok := secret.Data[corev1.TLSPrivateKeyKey]
if !ok {
return nil, nil, fmt.Errorf("key %s not found in secret %s", corev1.TLSPrivateKeyKey, name)
}

return cert, privateKey, nil
}

func getOrCreateIngressCertificateAndKey(ctx context.Context, kubeClient client.Client, name, namespace string, sans []string) ([]byte, []byte, error) {
c, p, err := getIngressCertificateAndKey(ctx, kubeClient, name, namespace)
if err != nil {
if k8serrors.IsNotFound(err) {
cert, privateKey, cErr := createSelfSignedCertificate(sans)
if cErr != nil {
return nil, nil, cErr
}

secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Type: corev1.SecretTypeTLS,
StringData: map[string]string{
corev1.TLSPrivateKeyKey: string(privateKey),
corev1.TLSCertKey: string(cert),
},
}
cErr = kubeClient.Create(ctx, secret)
if cErr != nil {
return nil, nil, fmt.Errorf("creating secret %s: %w", secret.Name, err)
}
return cert, privateKey, nil
} else {
return nil, nil, fmt.Errorf("getting secret %s: %w", name, err)
}
}
return c, p, nil
}

func createSelfSignedCertificate(sans []string) ([]byte, []byte, error) {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("generating private key: %w", err)
}

keyUsage := x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign
notBefore := time.Now()
notAfter := notBefore.Add(certificateValidLength)

serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
nabuskey marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, nil, fmt.Errorf("generating certificate serial number: %w", err)
}

cert := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{certificateOrgName},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: keyUsage,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IsCA: true,
DNSNames: sans,
}

certBytes, err := x509.CreateCertificate(rand.Reader, &cert, &cert, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, nil, fmt.Errorf("creating certificate: %w", err)
}

var certB bytes.Buffer
var keyB bytes.Buffer
err = pem.Encode(io.Writer(&certB), &pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
if err != nil {
return nil, nil, fmt.Errorf("encoding cert: %w", err)
}

certOut, err := io.ReadAll(&certB)
if err != nil {
return nil, nil, fmt.Errorf("reading buffer: %w", err)
}

privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
return nil, nil, fmt.Errorf("marshal private key: %w", err)
}

err = pem.Encode(io.Writer(&keyB), &pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyBytes})
if err != nil {
return nil, nil, fmt.Errorf("encoding private key: %w", err)
}
privateKeyOut, err := io.ReadAll(&keyB)
if err != nil {
return nil, nil, fmt.Errorf("reading buffer: %w", err)
}

return certOut, privateKeyOut, nil
}

func setupSelfSignedCertificate(ctx context.Context, logger logr.Logger, kubeclient client.Client, config util.CorePackageTemplateConfig) ([]byte, error) {
if err := k8s.EnsureNamespace(ctx, kubeclient, globals.NginxNamespace); err != nil {
return nil, err
}

sans := []string{
globals.DefaultHostName,
globals.DefaultSANWildcard,
}
if config.Host != globals.DefaultHostName {
sans = []string{
config.Host,
fmt.Sprintf("*.%s", config.Host),
}
}

logger.V(1).Info("Creating/getting certificate", "host", config.Host, "sans", sans)
cert, _, err := getOrCreateIngressCertificateAndKey(ctx, kubeclient, globals.SelfSignedCertSecretName, globals.NginxNamespace, sans)
if err != nil {
return nil, err
}

logger.V(1).Info("Creating secret for certificate", "host", config.Host)
err = createIngressCertificateSecret(ctx, kubeclient, cert)
if err != nil {
return nil, err
}
return cert, nil
}
88 changes: 88 additions & 0 deletions pkg/build/tls_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package build

import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"testing"

"github.com/cnoe-io/idpbuilder/globals"
"github.com/stretchr/testify/mock"
"gotest.tools/v3/assert"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
)

type fakeKubeClient struct {
mock.Mock
client.Client
}

func (f *fakeKubeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
args := f.Called(ctx, key, obj, opts)
return args.Error(0)
}

func (f *fakeKubeClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
args := f.Called(ctx, obj, opts)
return args.Error(0)
}

func TestCreateSelfSignedCertificate(t *testing.T) {
sans := []string{"cnoe.io", "*.cnoe.io"}
c, k, err := createSelfSignedCertificate(sans)
assert.NilError(t, err)
_, err = tls.X509KeyPair(c, k)
assert.NilError(t, err)

block, _ := pem.Decode(c)
assert.Equal(t, "CERTIFICATE", block.Type)
cert, err := x509.ParseCertificate(block.Bytes)
assert.NilError(t, err)

assert.Equal(t, 2, len(cert.DNSNames))
expected := map[string]struct{}{
"cnoe.io": {},
"*.cnoe.io": {},
}

for _, s := range cert.DNSNames {
_, ok := expected[s]
if ok {
delete(expected, s)
} else {
t.Fatalf("unexpected key %s found", s)
}
}
assert.Equal(t, 0, len(expected))
}

func TestGetOrCreateIngressCertificateAndKey(t *testing.T) {
ctx := context.Background()
fClient := new(fakeKubeClient)
fClient.On("Get", ctx, client.ObjectKey{Name: globals.SelfSignedCertSecretName, Namespace: globals.NginxNamespace}, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
arg := args.Get(2).(*corev1.Secret)
d := map[string][]byte{
corev1.TLSPrivateKeyKey: []byte("abc"),
corev1.TLSCertKey: []byte("abc"),
}
arg.Data = d
}).Return(nil)

_, _, err := getOrCreateIngressCertificateAndKey(ctx, fClient, globals.SelfSignedCertSecretName, globals.NginxNamespace, []string{globals.DefaultHostName, globals.DefaultSANWildcard})
assert.NilError(t, err)
fClient.AssertExpectations(t)

fClient = new(fakeKubeClient)
fClient.On("Get", ctx, client.ObjectKey{Name: globals.SelfSignedCertSecretName, Namespace: globals.NginxNamespace}, mock.Anything, mock.Anything).
Return(k8serrors.NewNotFound(schema.GroupResource{}, "name"))
fClient.On("Create", ctx, mock.Anything, mock.Anything).Return(nil)

c, k, err := getOrCreateIngressCertificateAndKey(ctx, fClient, globals.SelfSignedCertSecretName, globals.NginxNamespace, []string{globals.DefaultHostName, globals.DefaultSANWildcard})
assert.NilError(t, err)
_, err = tls.X509KeyPair(c, k)
assert.NilError(t, err)
}
3 changes: 2 additions & 1 deletion pkg/cmd/create/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"

"github.com/cnoe-io/idpbuilder/api/v1alpha1"
"github.com/cnoe-io/idpbuilder/globals"
"github.com/cnoe-io/idpbuilder/pkg/build"
"github.com/cnoe-io/idpbuilder/pkg/cmd/helpers"
"github.com/cnoe-io/idpbuilder/pkg/k8s"
Expand Down Expand Up @@ -53,7 +54,7 @@ func init() {
CreateCmd.PersistentFlags().StringVar(&kindConfigPath, "kind-config", "", "Path of the kind config file to be used instead of the default.")

// in-cluster resources related flags
CreateCmd.PersistentFlags().StringVar(&host, "host", "cnoe.localtest.me", "Host name to access resources in this cluster.")
CreateCmd.PersistentFlags().StringVar(&host, "host", globals.DefaultHostName, "Host name to access resources in this cluster.")
nabuskey marked this conversation as resolved.
Show resolved Hide resolved
CreateCmd.PersistentFlags().StringVar(&ingressHost, "ingress-host-name", "", "Host name used by ingresses. Useful when you have another proxy in front of ingress-nginx that idpbuilder provisions.")
CreateCmd.PersistentFlags().StringVar(&protocol, "protocol", "https", "Protocol to use to access web UIs. http or https.")
CreateCmd.PersistentFlags().StringVar(&port, "port", "8443", "Port number under which idpBuilder tools are accessible.")
Expand Down
Loading
Loading