certificate overhaul

This commit is contained in:
Hyatt 2023-03-25 15:06:58 -05:00
parent c7fef651b2
commit 42e154c6ab
Signed by: nhyatt
GPG Key ID: C50D0BBB5BC40BEA
18 changed files with 406 additions and 166 deletions

3
.gitignore vendored
View File

@ -1 +1,2 @@
.vscode
.vscode
k8s/ingress.yaml

View File

@ -11,7 +11,6 @@ import (
"encoding/json"
"net/http"
"mutating-webhook/internal/certificate"
"mutating-webhook/internal/config"
"mutating-webhook/internal/operations"
@ -37,16 +36,9 @@ func strictTransport(w http.ResponseWriter) {
w.Header().Add("Strict-Transport-Security", "max-age=63072000")
}
func httpServer() {
var serverCertificate tls.Certificate
if config.DefaultConfig().WebServerCertificate == "" || cfg.WebServerKey == "" {
log.Printf("[INFO] No webserver certificate configured, automatically generating self signed certificate.")
serverCertificate = certificate.CreateServerCert()
} else {
log.Fatal("[FATAL] Code to support external webserver certificate is not complete yet. ./cmd/webhook/httpServer.go:36")
// read certificate from files
// check for errors
}
func httpServer(cfg *config.Config) {
serverCertificate, _ := tls.X509KeyPair(append([]byte(cfg.CertCert), []byte(cfg.CACert)...), []byte(cfg.CertPrivateKey))
path := http.NewServeMux()
connection := &http.Server{
@ -73,7 +65,7 @@ func httpServer() {
ah := &admissionHandler{
decoder: serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer(),
config: &cfg,
config: cfg,
}
// pod admission

View File

@ -29,7 +29,7 @@ func main() {
// initialize application configuration
cfg = config.Init()
go httpServer()
go httpServer(&cfg)
forever()
}

View File

@ -1,6 +1,6 @@
---
allow-admin-nomutate: false
allow-admin-nomutate-toggle: 2d77b689-dc14-40a5-8971-34c62999335c
allow-admin-nomutate-toggle: 7b068a99-c02b-410a-bd59-3514bac85e7a
dockerhub-registry: registry.hub.docker.com
mutate-ignored-images:
- goharbor/chartmuseum-photon
@ -12,4 +12,4 @@ mutate-ignored-images:
- goharbor/harbor-portal
- goharbor/redis-photon
- goharbor/registry-photon
- goharbor/trivy-adapter-photon
- goharbor/trivy-adapter-photon

View File

@ -2,18 +2,19 @@ package certificate
import (
"bytes"
"fmt"
"log"
"strconv"
"time"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
)
func CreateCA() ([]byte, []byte, []byte, error) {
func CreateCA(privateKey string) (string, error) {
serial, _ := strconv.ParseInt(time.Now().Format("20060102150405"), 10, 64)
ca := &x509.Certificate{
SerialNumber: big.NewInt(serial),
@ -37,14 +38,18 @@ func CreateCA() ([]byte, []byte, []byte, error) {
SignatureAlgorithm: x509.SHA384WithRSA,
}
keyPair, err := rsa.GenerateKey(rand.Reader, 4096)
pemKey, _ := pem.Decode([]byte(privateKey))
if pemKey == nil || pemKey.Type != "RSA PRIVATE KEY" {
return "", fmt.Errorf("failed to decode PEM block containing private key")
}
keyPair, err := x509.ParsePKCS1PrivateKey(pemKey.Bytes)
if err != nil {
return []byte(""), []byte(""), []byte(""), err
return "", err
}
certBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &keyPair.PublicKey, keyPair)
if err != nil {
return []byte(""), []byte(""), []byte(""), err
return "", err
}
c := new(bytes.Buffer)
@ -53,17 +58,6 @@ func CreateCA() ([]byte, []byte, []byte, error) {
Bytes: certBytes,
})
k := new(bytes.Buffer)
pem.Encode(k, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(keyPair),
})
p := new(bytes.Buffer)
pem.Encode(p, &pem.Block{
Type: "PUBLIC KEY",
Bytes: x509.MarshalPKCS1PublicKey(&keyPair.PublicKey),
})
return c.Bytes(), k.Bytes(), p.Bytes(), nil
log.Printf("[DEBUG] Generated Certificate Authority Certificate:\n%s", c)
return c.String(), nil
}

View File

@ -1,72 +0,0 @@
package certificate
import (
"bytes"
"strconv"
"time"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
)
func CreateCert() ([]byte, []byte, []byte, error) {
serial, _ := strconv.ParseInt(time.Now().Format("20060102150405"), 10, 64)
ca := &x509.Certificate{
SerialNumber: big.NewInt(serial + 1),
Subject: pkix.Name{
Organization: []string{"Kubernetes Mutating Webserver"},
Country: []string{"K8S"},
Province: []string{"Cluster Service"},
Locality: []string{"Cluster Local"},
//StreetAddress: []string{""},
//PostalCode: []string{""},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 6, 0),
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth,
x509.ExtKeyUsageServerAuth,
},
DNSNames: []string{
"svc.cluster.local",
"*.svc.cluster.local",
},
SubjectKeyId: []byte{1, 2, 3, 4, 6},
KeyUsage: x509.KeyUsageDigitalSignature,
SignatureAlgorithm: x509.SHA384WithRSA,
}
keyPair, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return []byte(""), []byte(""), []byte(""), err
}
certBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &keyPair.PublicKey, keyPair)
if err != nil {
return []byte(""), []byte(""), []byte(""), err
}
c := new(bytes.Buffer)
pem.Encode(c, &pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
})
k := new(bytes.Buffer)
pem.Encode(k, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(keyPair),
})
p := new(bytes.Buffer)
pem.Encode(p, &pem.Block{
Type: "PUBLIC KEY",
Bytes: x509.MarshalPKCS1PublicKey(&keyPair.PublicKey),
})
return c.Bytes(), k.Bytes(), p.Bytes(), nil
}

View File

@ -0,0 +1,53 @@
package certificate
import (
"bytes"
"fmt"
"log"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
)
func CreateCSR(privateKey string) (string, error) {
csr := x509.CertificateRequest{
Subject: pkix.Name{
Organization: []string{"Kubernetes Mutating Webserver"},
Country: []string{"K8S"},
Province: []string{"Cluster Service"},
Locality: []string{"Cluster Local"},
//StreetAddress: []string{""},
//PostalCode: []string{""},
},
DNSNames: []string{
"svc.cluster.local",
"*.svc.cluster.local",
},
SignatureAlgorithm: x509.SHA384WithRSA,
}
pemKey, _ := pem.Decode([]byte(privateKey))
if pemKey == nil || pemKey.Type != "RSA PRIVATE KEY" {
return "", fmt.Errorf("failed to decode PEM block containing private key")
}
keyPair, err := x509.ParsePKCS1PrivateKey(pemKey.Bytes)
if err != nil {
return "", err
}
csrData, err := x509.CreateCertificateRequest(rand.Reader, &csr, keyPair)
if err != nil {
return "", err
}
c := new(bytes.Buffer)
pem.Encode(c, &pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csrData,
})
log.Printf("[TRACE] Generated Host CSR:\n%s", c.String())
return c.String(), nil
}

View File

@ -0,0 +1,15 @@
package certificate
import (
"crypto/rand"
"crypto/rsa"
)
func CreateRSAKeyPair(bytes int) (*rsa.PrivateKey, error) {
keyPair, err := rsa.GenerateKey(rand.Reader, bytes)
if err != nil {
return &rsa.PrivateKey{}, err
}
return keyPair, nil
}

View File

@ -1,43 +0,0 @@
package certificate
import (
"bytes"
"log"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/pem"
)
func CreateServerCert() tls.Certificate {
caCertPem, caPrivKeyPem, _, _ := CreateCA()
certCertPem, certPrivKeyPem, certPublicKeyPem, _ := CreateCert()
caCertBlob, _ := pem.Decode(caCertPem)
caCert, _ := x509.ParseCertificate(caCertBlob.Bytes)
caPrivKeyBlob, _ := pem.Decode(caPrivKeyPem)
caPrivKey, _ := x509.ParsePKCS1PrivateKey(caPrivKeyBlob.Bytes)
certCertBlob, _ := pem.Decode(certCertPem)
certCert, _ := x509.ParseCertificate(certCertBlob.Bytes)
certPublicKeyBlob, _ := pem.Decode(certPublicKeyPem)
certPublicKey, _ := x509.ParsePKCS1PublicKey(certPublicKeyBlob.Bytes)
signedCert, err := x509.CreateCertificate(rand.Reader, certCert, caCert, certPublicKey, caPrivKey)
if err != nil {
log.Fatalf("[FATAL] CreateCertificate: %v", err)
}
serverCertPem := new(bytes.Buffer)
pem.Encode(serverCertPem, &pem.Block{
Type: "CERTIFICATE",
Bytes: signedCert,
})
serverCert, err := tls.X509KeyPair(append(serverCertPem.Bytes(), caCertPem...), certPrivKeyPem)
if err != nil {
log.Fatalf("[FATAL] x509KeyPair: %v", err)
}
return serverCert
}

View File

@ -0,0 +1,74 @@
package certificate
import (
"bytes"
"fmt"
"strconv"
"time"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
)
func SignCert(caCertPem, caPrivKeyPem, csrPem string) (string, error) {
caCertData, _ := pem.Decode([]byte(caCertPem))
caCert, err := x509.ParseCertificate(caCertData.Bytes)
if err != nil {
return "", fmt.Errorf("parse cert %v", err)
}
pemKey, _ := pem.Decode([]byte(caPrivKeyPem))
if pemKey == nil || pemKey.Type != "RSA PRIVATE KEY" {
return "", fmt.Errorf("failed to decode PEM block containing private key")
}
keyPair, err := x509.ParsePKCS1PrivateKey(pemKey.Bytes)
if err != nil {
return "", fmt.Errorf("private key %v", err)
}
csrData, _ := pem.Decode([]byte(csrPem))
csr, err := x509.ParseCertificateRequest(csrData.Bytes)
if err != nil {
return "", fmt.Errorf("parse csr %v", err)
}
serial, _ := strconv.ParseInt(time.Now().Format("20060102150405"), 10, 64)
certTemplate := x509.Certificate{
SerialNumber: big.NewInt(serial + 1),
Issuer: caCert.Issuer,
Subject: pkix.Name{
Organization: []string{"Kubernetes Mutating Webserver"},
Country: csr.Subject.Country,
Province: []string{"Cluster Service"},
Locality: []string{"Cluster Local"},
StreetAddress: csr.Subject.StreetAddress,
PostalCode: csr.Subject.PostalCode,
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 6, 0),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageClientAuth,
x509.ExtKeyUsageServerAuth,
},
DNSNames: csr.DNSNames,
Signature: csr.Signature,
SignatureAlgorithm: csr.SignatureAlgorithm,
PublicKey: csr.PublicKey,
PublicKeyAlgorithm: csr.PublicKeyAlgorithm,
}
certBytes, err := x509.CreateCertificate(rand.Reader, &certTemplate, caCert, csr.PublicKey, keyPair)
if err != nil {
return "", fmt.Errorf("sign %v", err)
}
c := new(bytes.Buffer)
pem.Encode(c, &pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
})
return c.String(), nil
}

View File

@ -35,9 +35,15 @@ type Config struct {
// mutation configuration
AllowAdminNoMutate bool `env:"allow_admin_nomutate" default:"false"`
AllowAdminNoMutateToggle string `env:"allow_admin_nomutate_toggle" default:"2d77b689-dc14-40a5-8971-34c62999335c"`
AllowAdminNoMutateToggle string `env:"allow_admin_nomutate_toggle" default:"7b068a99-c02b-410a-bd59-3514bac85e7a"`
DockerhubRegistry string `env:"dockerhub_registry" default:"registry.hub.docker.com"`
MutateIgnoredImages []string `ignored:"true"`
// certificate configuration
CACert string `env:"ca_cert"`
CAPrivateKey string `env:"ca_private_key"`
CertCert string `env:"cert_cert"`
CertPrivateKey string `env:"cert_private_key"`
}
// DefaultConfig initializes the config variable for use with a prepared set of defaults.

View File

@ -9,10 +9,18 @@ import (
)
type configFileStruct struct {
AllowAdminNoMutate bool `yaml:"allow-admin-nomutate"`
AllowAdminNoMutateToggle string `yaml:"allow-admin-nomutate-toggle"`
DockerhubRegistry string `yaml:"dockerhub-registry"`
MutateIgnoredImages []string `yaml:"mutate-ignored-images"`
AllowAdminNoMutate bool `yaml:"allow-admin-nomutate"`
AllowAdminNoMutateToggle string `yaml:"allow-admin-nomutate-toggle"`
DockerhubRegistry string `yaml:"dockerhub-registry"`
MutateIgnoredImages []string `yaml:"mutate-ignored-images"`
CertificateAuthority CertStruct `yaml:"certificate-authority"`
Certificate CertStruct `yaml:"certificate"`
}
type CertStruct struct {
Certificate string `yaml:"certificate"`
PrivateKey string `yaml:"private-key"`
PublicKey string `yaml:"public-key"`
}
func getConfigFileData(fileLocation string) (configFileStruct, error) {

View File

@ -1,8 +1,13 @@
package config
import (
"bytes"
"crypto/x509"
"encoding/pem"
"flag"
"fmt"
"log"
"mutating-webhook/internal/certificate"
"os"
"reflect"
"strings"
@ -70,25 +75,108 @@ func Init() Config {
}
time.Now().Format(cfg.TimeFormat)
// print running config
printRunningConfig(&cfg, cfgInfo)
// read config file
configFileData, err := getConfigFileData(cfg.ConfigFile)
if err != nil {
log.Fatalf("[FATAL] Unable to read configuration file")
}
if cfg.AllowAdminNoMutate == false {
cfg.AllowAdminNoMutate = configFileData.AllowAdminNoMutate
updateValues(&cfg, configFileData)
// Generate certificates if needed
if err := certificateInit(&cfg); err != nil {
log.Fatalf("[FATAL] Unable to initialize certificate data: %v", err)
}
if cfg.AllowAdminNoMutateToggle == "2d77b689-dc14-40a5-8971-34c62999335c" {
cfg.AllowAdminNoMutateToggle = configFileData.AllowAdminNoMutateToggle
}
if cfg.DockerhubRegistry == "registry.hub.docker.com" {
cfg.DockerhubRegistry = configFileData.DockerhubRegistry
}
cfg.MutateIgnoredImages = configFileData.MutateIgnoredImages
// print running config
printRunningConfig(&cfg, cfgInfo)
log.Println("[INFO] initialization sequence complete")
return cfg
}
func updateValues(cfg *Config, configFileData configFileStruct) {
if cfg.AllowAdminNoMutate == false && configFileData.AllowAdminNoMutate != false {
cfg.AllowAdminNoMutate = configFileData.AllowAdminNoMutate
}
if cfg.AllowAdminNoMutateToggle == "7b068a99-c02b-410a-bd59-3514bac85e7a" && configFileData.AllowAdminNoMutateToggle != "2d77b689-dc14-40a5-8971-34c62999335c" {
cfg.AllowAdminNoMutateToggle = configFileData.AllowAdminNoMutateToggle
}
if cfg.DockerhubRegistry == "registry.hub.docker.com" && configFileData.DockerhubRegistry != "registry.hub.docker.com" {
cfg.DockerhubRegistry = configFileData.DockerhubRegistry
}
if len(configFileData.MutateIgnoredImages) != 0 {
cfg.MutateIgnoredImages = configFileData.MutateIgnoredImages
}
if len(configFileData.CertificateAuthority.Certificate) != 0 {
cfg.CACert = configFileData.CertificateAuthority.Certificate
}
if len(configFileData.CertificateAuthority.PrivateKey) != 0 {
cfg.CAPrivateKey = configFileData.CertificateAuthority.PrivateKey
}
if len(configFileData.Certificate.Certificate) != 0 {
cfg.CertCert = configFileData.Certificate.Certificate
}
if len(configFileData.Certificate.PrivateKey) != 0 {
cfg.CertPrivateKey = configFileData.Certificate.PrivateKey
}
}
func certificateInit(cfg *Config) error {
// certificate authority private key does not exist, generate key pair
if len(cfg.CAPrivateKey) == 0 {
log.Printf("[TRACE] No certificate authority private key detected")
keyPair, err := certificate.CreateRSAKeyPair(4096)
if err != nil {
return fmt.Errorf("Create RSA Key (%v)", err)
}
// pem encode private key
k := new(bytes.Buffer)
pem.Encode(k, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(keyPair),
})
cfg.CAPrivateKey = k.String()
}
// certificate authority certificate is missing, create it
if len(cfg.CACert) == 0 {
log.Printf("[TRACE] No certificate authority certificate detected")
caCert, err := certificate.CreateCA(cfg.CAPrivateKey)
if err != nil {
return fmt.Errorf("Create CA (%v)", err)
}
cfg.CACert = caCert
}
// certificate private key does not exist, generate key pair
if len(cfg.CertPrivateKey) == 0 {
log.Printf("[TRACE] No server private key detected")
keyPair, err := certificate.CreateRSAKeyPair(4096)
if err != nil {
return fmt.Errorf("Create RSA Key (%v)", err)
}
// pem encode private key
k := new(bytes.Buffer)
pem.Encode(k, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(keyPair),
})
cfg.CertPrivateKey = k.String()
}
// certificate certificate is missing, create it
if len(cfg.CertCert) == 0 {
log.Printf("[TRACE] No server certificate detected")
csr, err := certificate.CreateCSR(cfg.CertPrivateKey)
if err != nil {
return fmt.Errorf("Create CSR (%v)", err)
}
cert, err := certificate.SignCert(cfg.CACert, cfg.CAPrivateKey, csr)
if err != nil {
return fmt.Errorf("Sign Cert (%v)", err)
}
cfg.CertCert = cert
}
return nil
}

19
k8s/admission.yaml Normal file
View File

@ -0,0 +1,19 @@
---
apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: deployment-validation
webhooks:
- name: deployment-validation.default.svc
clientConfig:
service:
name: admission-server
namespace: default
path: "/validate/deployments"
caBundle: "${CA_BUNDLE}"
rules:
- operations: ["CREATE","DELETE"]
apiGroups: ["apps"]
apiVersions: ["v1"]
resources: ["deployments"]
failurePolicy: Ignore

18
k8s/configmap.yaml Normal file
View File

@ -0,0 +1,18 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: webhook-config
data:
config.yaml: |-
mutate-ignored-images:
- goharbor/chartmuseum-photon
- goharbor/harbor-core
- goharbor/harbor-db
- goharbor/harbor-jobservice
- goharbor/notary-server-photon
- goharbor/notary-signer-photon
- goharbor/harbor-portal
- goharbor/redis-photon
- goharbor/registry-photon
- goharbor/trivy-adapter-photon

55
k8s/daemonset.yaml Normal file
View File

@ -0,0 +1,55 @@
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: mutating-webhool
labels:
app: mutating-webhook
spec:
selector:
matchLabels:
app: mutating-webhook
template:
metadata:
labels:
app: mutating-webhook
spec:
volumes:
- name: config
configMap:
name: webhook-config
defaultMode: 493
optional: false
containers:
- name: mutating-webhook
imagePullPolicy: Always
env:
- name: DOCKERHUB_REGISTRY
value: registry.c.test-chamber-13.lan/dockerhub
- name: ALLOW_ADMIN_NOMUTATE
value: "true"
- name: CONFIG_FILE
value: /tmp/config/config.yaml
image: registry.c.test-chamber-13.lan/library/webhook:latest
volumeMounts:
- name: config
mountPath: /tmp/config
livenessProbe:
httpGet:
path: /healthcheck
port: 8443
scheme: HTTPS
periodSeconds: 15
initialDelaySeconds: 10
timeoutSeconds: 2
failureThreshold: 1
readinessProbe:
httpGet:
path: /healthcheck
port: 8443
scheme: HTTPS
initialDelaySeconds: 10
timeoutSeconds: 2
failureThreshold: 1
terminationGracePeriodSeconds: 30

14
k8s/service.yaml Normal file
View File

@ -0,0 +1,14 @@
---
apiVersion: v1
kind: Service
metadata:
name: webhook
spec:
ports:
- name: https
protocol: TCP
port: 443
targetPort: 8443
selector:
app: mutating-webhook
type: ClusterIP

18
k8s/webhook.yaml Normal file
View File

@ -0,0 +1,18 @@
---
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
name: pod-mutation
webhooks:
- name: pod-mutation.default.svc
clientConfig:
service:
name: webhook
namespace: default
path: "/api/v1/mutate/pod"
rules:
- operations: ["CREATE", ]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
failurePolicy: Ignore