上篇我们手动添加证书很麻烦,这次我们自动生成证书,并用client-go创建mutatingwebhookconfigurations,validatingwebhookconfigurations
package main
import (
"bytes"
"context"
cryptorand "crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"log"
"math/big"
"os"
"time"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
func main() {
var caPEM, serverCertPEM, serverPrivKeyPEM *bytes.Buffer
ca := &x509.Certificate{
SerialNumber: big.NewInt(2023),
Subject: pkix.Name{
Country: []string{"CN"},
Province: []string{"Beijing"},
Locality: []string{"Beijing"},
Organization: []string{"sasa.io"},
OrganizationalUnit: []string{"sasa.io"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
caPrivkey, err := rsa.GenerateKey(cryptorand.Reader, 4096)
if err != nil {
fmt.Println(err)
}
caBytes, err := x509.CreateCertificate(cryptorand.Reader, ca, ca, &caPrivkey.PublicKey, caPrivkey)
if err != nil {
fmt.Println(err)
}
caPEM = new(bytes.Buffer)
_ = pem.Encode(caPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: caBytes,
})
dnsNames := []string{
"admission-registry",
"admission-registry.default", "admission-registry.default.svc"}
commonName := "admission-registry.default.svc"
cert := &x509.Certificate{
DNSNames: dnsNames,
SerialNumber: big.NewInt(1658),
Subject: pkix.Name{
CommonName: commonName,
Organization: []string{"sasa.io"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1, 0, 0),
SubjectKeyId: []byte{1, 2, 3, 4, 6},
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature,
}
serverPrivkey, err := rsa.GenerateKey(cryptorand.Reader, 4096)
if err != nil {
fmt.Println(err)
}
serverCertBytes, err := x509.CreateCertificate(cryptorand.Reader, cert, ca, &serverPrivkey.PublicKey, caPrivkey)
if err != nil {
fmt.Println(err)
}
serverCertPEM = new(bytes.Buffer)
_ = pem.Encode(serverCertPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: serverCertBytes,
})
serverPrivKeyPEM = new(bytes.Buffer)
_ = pem.Encode(serverPrivKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(serverPrivkey),
})
err = os.MkdirAll("/etc/webhook/certs/", 0666)
if err != nil {
log.Panic(err)
}
err = WriteFile("/etc/webhook/certs/tls.cert", serverCertPEM)
if err != nil {
log.Panic(err)
}
err = WriteFile("/etc/webhook/certs/tls.key", serverPrivKeyPEM)
if err != nil {
log.Panic(err)
}
log.Println("webhook server tls generated successfully")
if err = CreateAdmissionConfig(caPEM); err != nil {
fmt.Println(err)
}
log.Println("webhook configuration generated successfully")
}
func WriteFile(filepath string, sCert *bytes.Buffer) error {
f, err := os.Create(filepath)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(sCert.Bytes())
if err != nil {
return err
}
return nil
}
func initKubeClient() (*kubernetes.Clientset, error) {
var (
err error
config *rest.Config
)
if config, err = rest.InClusterConfig(); err != nil {
return nil, err
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, err
}
return clientset, nil
}
func CreateAdmissionConfig(caCert *bytes.Buffer) error {
var (
webhookNamespace, _ = os.LookupEnv("WEBHOOK_NAMESPACE")
mutationCfgName, _ = os.LookupEnv("MUTATE_CONFIG")
validateCfgName, _ = os.LookupEnv("VALIDATE_CONFIG")
webhookService, _ = os.LookupEnv("WEBHOOK_SERVICE")
validatePath, _ = os.LookupEnv("VALIDATE_PATH")
mutationPath, _ = os.LookupEnv("MUTATE_PATH")
)
clientset, err := initKubeClient()
if err != nil {
return err
}
ctx := context.Background()
if validateCfgName != "" {
validateConfig := &admissionregistrationv1.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: validateCfgName,
},
Webhooks: []admissionregistrationv1.ValidatingWebhook{
{
Name: "io.ydzs.admission-registry",
ClientConfig: admissionregistrationv1.WebhookClientConfig{
CABundle: caCert.Bytes(),
Service: &admissionregistrationv1.ServiceReference{
Name: webhookService,
Namespace: webhookNamespace,
Path: &validatePath,
},
},
Rules: []admissionregistrationv1.RuleWithOperations{
{
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{""},
APIVersions: []string{"v1"},
Resources: []string{"pods"},
},
},
},
FailurePolicy: func() *admissionregistrationv1.FailurePolicyType {
pt := admissionregistrationv1.Fail
return &pt
}(),
AdmissionReviewVersions: []string{"v1"},
SideEffects: func() *admissionregistrationv1.SideEffectClass {
se := admissionregistrationv1.SideEffectClassNone
return &se
}(),
},
},
}
validateAdmissionClient := clientset.AdmissionregistrationV1().ValidatingWebhookConfigurations()
_, err := validateAdmissionClient.Get(ctx, validateCfgName, metav1.GetOptions{})
if err != nil {
if errors.IsNotFound(err) {
if _, err = validateAdmissionClient.Create(ctx, validateConfig, metav1.CreateOptions{}); err != nil {
return err
}
} else {
return err
}
} else {
if _, err = validateAdmissionClient.Update(ctx, validateConfig, metav1.UpdateOptions{}); err != nil {
return err
}
}
}
if mutationCfgName != "" {
mutateConfig := &admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: mutationCfgName,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{{
Name: "io.ydzs.admission-registry-mutate",
ClientConfig: admissionregistrationv1.WebhookClientConfig{
CABundle: caCert.Bytes(), // CA bundle created earlier
Service: &admissionregistrationv1.ServiceReference{
Name: webhookService,
Namespace: webhookNamespace,
Path: &mutationPath,
},
},
Rules: []admissionregistrationv1.RuleWithOperations{{Operations: []admissionregistrationv1.OperationType{
admissionregistrationv1.Create},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"apps", ""},
APIVersions: []string{"v1"},
Resources: []string{"deployments", "services"},
},
}},
FailurePolicy: func() *admissionregistrationv1.FailurePolicyType {
pt := admissionregistrationv1.Fail
return &pt
}(),
AdmissionReviewVersions: []string{"v1"},
SideEffects: func() *admissionregistrationv1.SideEffectClass {
se := admissionregistrationv1.SideEffectClassNone
return &se
}(),
}},
}
mutateAdmissionClient := clientset.AdmissionregistrationV1().MutatingWebhookConfigurations()
_, err := mutateAdmissionClient.Get(ctx, mutationCfgName, metav1.GetOptions{})
if err != nil {
if errors.IsNotFound(err) {
if _, err = mutateAdmissionClient.Create(ctx, mutateConfig, metav1.CreateOptions{}); err != nil {
return err
}
} else {
return err
}
} else {
if _, err = mutateAdmissionClient.Update(ctx, mutateConfig, metav1.UpdateOptions{}); err != nil {
return err
}
}
}
return nil
}
dockerfile
# Build the webhook binary
FROM golang:1.19 as builder
RUN apt-get -y update && apt-get -y install upx
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# Copy the go source
COPY main.go main.go
# Build
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
ENV GO111MODULE=on
ENV GOPROXY="https://goproxy.cn"
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download && \
go build -a -o admission-registry main.go && \
upx admission-registry
FROM alpine:3.9.2
COPY --from=builder /workspace/admission-registry .
ENTRYPOINT ["/admission-registry"]
基于rbac的initcontainer创建证书,跟主容器共享一个目录,使用deployment部署
apiVersion: v1
kind: ServiceAccount
metadata:
name: admission-registry-sa
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: admission-registry-role
rules:
- verbs: ["*"]
resources: ["validatingwebhookconfigurations", "mutatingwebhookconfigurations"]
apiGroups: ["admissionregistration.k8s.io"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: admission-registry-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: admission-registry-role
subjects:
- kind: ServiceAccount
name: admission-registry-sa
namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: admission-registry
labels:
app: admission-registry
spec:
selector:
matchLabels:
app: admission-registry
template:
metadata:
labels:
app: admission-registry
spec:
serviceAccountName: admission-registry-sa
initContainers:
- image: webhook-tls-all-in-one:v1.0
imagePullPolicy: IfNotPresent
name: webhook-init
env:
- name: WEBHOOK_NAMESPACE
value: default
- name: MUTATE_CONFIG
value: admission-registry-mutate
- name: VALIDATE_CONFIG
value: admission-registry
- name: WEBHOOK_SERVICE
value: admission-registry
- name: VALIDATE_PATH
value: /validate
- name: MUTATE_PATH
value: /mutate
volumeMounts:
- mountPath: /etc/webhook/certs
name: webhook-certs
containers:
- name: webhook
image: webhook-validate-mutate:v1.0
imagePullPolicy: IfNotPresent
env:
- name: WHITELIST_REGISTRIES
value: "docker.io,gcr.io"
ports:
- containerPort: 443
volumeMounts:
- name: webhook-certs
mountPath: /etc/webhook/certs
readOnly: true
volumes:
- name: webhook-certs
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: admission-registry
labels:
app: admission-registry
spec:
ports:
- port: 443
targetPort: 443
selector:
app: admission-registry