Skip to content

Commit

Permalink
issue self signed certificate (#316)
Browse files Browse the repository at this point in the history
Signed-off-by: Manabu McCloskey <[email protected]>
  • Loading branch information
nabuskey authored Jun 21, 2024
1 parent ef0fc09 commit ee78c16
Show file tree
Hide file tree
Showing 18 changed files with 381 additions and 38 deletions.
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 }}
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)
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.")
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

0 comments on commit ee78c16

Please sign in to comment.