Skip to content

Commit

Permalink
Generate self signed certs for webhook (#15)
Browse files Browse the repository at this point in the history
* Generate self signed certs for webhook

Signed-off-by: Matthew DeVenny <matt@boxboat.com>
  • Loading branch information
matthewdevenny authored Jun 8, 2021
1 parent 7952b84 commit 2a1071c
Show file tree
Hide file tree
Showing 3 changed files with 331 additions and 165 deletions.
137 changes: 124 additions & 13 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,44 +20,149 @@ import (
"context"
"crypto/tls"
"fmt"
dockhand "github.com/boxboat/dockhand-secrets-operator/pkg/apis/dockhand.boxboat.io/v1alpha1"
"github.com/boxboat/dockhand-secrets-operator/pkg/common"
"github.com/boxboat/dockhand-secrets-operator/pkg/k8s"
"github.com/boxboat/dockhand-secrets-operator/pkg/webhook"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/client-go/tools/leaderelection"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)

type ServerArgs struct {
serverPort int
serverCert string
serverKey string
serviceName string
serviceNamespace string
selfSignCerts bool
serverPort int
serverCert string
serverKey string
serviceName string
serviceId string
serviceNamespace string
selfSignCerts bool
}

var (
serverArgs ServerArgs
)

func runServer(ctx context.Context) {
func runCertManager(ctx context.Context) {
lock, err := k8s.GetLeaseLock(serverArgs.serviceId, serverArgs.serviceName, serverArgs.serviceNamespace)
common.ExitIfError(err)

leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
Lock: lock,
LeaseDuration: 15 * time.Second,
RenewDeadline: 10 * time.Second,
RetryPeriod: 2 * time.Second,
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: onStartedLeading,
OnStoppedLeading: onStoppedLeading,
OnNewLeader: onNewLeader(serverArgs.serviceId),
},
WatchDog: nil,
ReleaseOnCancel: true,
Name: serverArgs.serviceId,
})

}

func onStartedLeading(ctx context.Context) {
common.Log.Infof("elected leader")
ensureTLSCertificateSecretInCluster(ctx)
for {
select {
case <-ctx.Done():
return
case <-time.After(time.Minute):
ensureTLSCertificateSecretInCluster(ctx)
}
}

}

func onStoppedLeading() {
common.Log.Infof("no longer leading")
}

func onNewLeader(id string) func(string) {
return func(newLeaderID string) {
if newLeaderID != id {
common.Log.Infof("%s elected new leader", newLeaderID)
}
}
}

func ensureTLSCertificateSecretInCluster(ctx context.Context) {

common.Log.Infof("checking certificate %s/%s", serverArgs.serviceNamespace, serverArgs.serviceName)
cert, err := k8s.GetServiceCertificate(ctx, serverArgs.serviceName, serverArgs.serviceNamespace)
if err != nil && !errors.IsNotFound(err) {
common.ExitIfError(err)
}
if cert == nil || common.ValidDaysRemaining(cert.Certificate[0]) < 30 {
common.Log.Infof("Renewing self signed certificate")
caPem, caKey, err := common.GenerateSelfSignedCA(serverArgs.serviceName + "-ca")
common.ExitIfError(err)
err = k8s.UpdateCABundleForWebhook(ctx, serverArgs.serviceName+".dockhand.boxboat.io", caPem)
common.ExitIfError(err)
dnsNames := []string{
serverArgs.serviceName + "." + serverArgs.serviceNamespace,
serverArgs.serviceName + "." + serverArgs.serviceNamespace + ".svc"}

cert, key, err := common.GenerateSignedCert(serverArgs.serviceName, dnsNames, caPem, caKey)
common.ExitIfError(err)
err = k8s.UpdateTlsCertificateSecret(ctx, serverArgs.serviceName, serverArgs.serviceNamespace, cert, key, caPem)
common.ExitIfError(err)

deploy, err := k8s.GetDeployment(ctx, serverArgs.serviceName, serverArgs.serviceNamespace)
common.ExitIfError(err)
checksum, err := k8s.GetSecretsChecksum(ctx, []string{serverArgs.serviceName}, serverArgs.serviceNamespace)
common.ExitIfError(err)
if deploy.Spec.Template.Annotations == nil {
deploy.Spec.Template.Annotations = make(map[string]string)
}
deploy.Spec.Template.Annotations[dockhand.SecretChecksumAnnotationKey] = checksum
_, err = k8s.UpdateDeployment(ctx, deploy, serverArgs.serviceNamespace)
if err != nil {
common.Log.Warnf("Could not update deployment %v", err)
}
}

}

func runServer(ctx context.Context) {
var err error
tlsPair := tls.Certificate{}
if serverArgs.selfSignCerts {
tlsPair, err = k8s.GetServiceCertificate(ctx, serverArgs.serviceName, serverArgs.serviceNamespace)
common.ExitIfError(err)
if err := k8s.UpdateCABundleForWebhook(ctx, serverArgs.serviceName + ".dockhand.boxboat.io", serverArgs.serviceNamespace); err != nil {
common.ExitIfError(err)
}
leaderCtx, cancel := context.WithCancel(ctx)
defer cancel()
go runCertManager(leaderCtx)
} else {
tlsPair, err = tls.LoadX509KeyPair(serverArgs.serverCert, serverArgs.serverKey)
common.ExitIfError(err)
}

attempt := 0
for {
if attempt < 10 {
cert, err := k8s.GetServiceCertificate(ctx, serverArgs.serviceName, serverArgs.serviceNamespace)
if errors.IsNotFound(err) {
time.Sleep(5 * time.Second)
} else if err != nil {
common.Log.Warnf("error retrieving certificate, %v", err)
} else {
tlsPair = *cert
break
}
attempt += 1
}
}

common.Log.Infof("Starting server")

server := &webhook.Server{
Server: &http.Server{
Addr: fmt.Sprintf(":%v", serverArgs.serverPort),
Expand Down Expand Up @@ -130,7 +235,13 @@ func init() {
&serverArgs.serverKey,
"key",
"/tls/server.key",
"x509 server certificate")
"x509 server key")

startServerCmd.Flags().StringVar(
&serverArgs.serviceId,
"webhook-id",
"",
"webhook server id")

startServerCmd.Flags().BoolVar(
&serverArgs.selfSignCerts,
Expand Down
119 changes: 119 additions & 0 deletions pkg/common/tls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package common

import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"time"
)

const EncryptionBits = 4096
const CertValidity = time.Hour * 24 * 365

func ValidDaysRemaining(pem []byte) int {
cert, err := x509.ParseCertificate(pem)
if err != nil {
Log.Warnf("could not parse certificate, %v", err)
return -1
}
return int(cert.NotAfter.Sub(time.Now()).Hours()/24)
}

func GenerateSelfSignedCA(commonName string) ([]byte, []byte, error) {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 256)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate serial number: %s", err)
}
template := &x509.Certificate{
SerialNumber: serialNumber,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
NotBefore: time.Now(),
NotAfter: time.Now().Add(CertValidity),
Subject: pkix.Name{CommonName: commonName},
IsCA: true,
MaxPathLenZero: true,
BasicConstraintsValid: true,
}

key, err := rsa.GenerateKey(rand.Reader, EncryptionBits)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate key: %s", err)
}

derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
return nil, nil, fmt.Errorf("failed to create certificate: %s", err)
}

var certPem, keyPem bytes.Buffer
if err := pem.Encode(&certPem, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
return nil, nil, fmt.Errorf("failed to encode certificate: %s", err)
}
if err := pem.Encode(&keyPem, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}); err != nil {
return nil,nil, fmt.Errorf("failed to encode key: %s", err)
}

return certPem.Bytes(), keyPem.Bytes(), nil
}

func GenerateSignedCert(commonName string, dnsNames []string, caPem []byte, caKey []byte) ([]byte,[]byte, error) {
key, err := rsa.GenerateKey(rand.Reader, EncryptionBits)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate key: %s", err)
}
csrDerBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{}, key)
if err != nil {
return nil,nil, fmt.Errorf("failed to generate csr: %s", err.Error())
}
csr, err := x509.ParseCertificateRequest(csrDerBytes)
if err != nil {
return nil,nil, fmt.Errorf("failed to parse csr: %s", err.Error())
}

serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 256)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil,nil, fmt.Errorf("failed to generate serial number: %s", err)
}
template := &x509.Certificate{
SerialNumber: serialNumber,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
NotBefore: time.Now(),
NotAfter: time.Now().Add(CertValidity),
Subject: pkix.Name{CommonName: commonName},
DNSNames: dnsNames,
}
caTlsPair, err := tls.X509KeyPair(caPem,caKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to load CA key pair: %s", err)
}

ca, err := x509.ParseCertificate(caTlsPair.Certificate[0])
if err != nil {
return nil,nil, fmt.Errorf("failed to parse CA certificate: %s", err)
}

derBytes, err := x509.CreateCertificate(rand.Reader, template, ca, csr.PublicKey, caTlsPair.PrivateKey)
if err != nil {
return nil,nil, fmt.Errorf("failed to create certificate: %s", err)
}

var certPem, keyPem bytes.Buffer
if err := pem.Encode(&certPem, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
return nil,nil, fmt.Errorf("failed to encode certificate: %s", err)
}
if err := pem.Encode(&keyPem, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}); err != nil {
return nil,nil, fmt.Errorf("failed to encode key: %s", err)
}

return certPem.Bytes(), keyPem.Bytes(), nil
}
Loading

0 comments on commit 2a1071c

Please sign in to comment.