Skip to content

Commit

Permalink
Merge pull request #31 from jsturtevant/fix-signer-name
Browse files Browse the repository at this point in the history
Updates for gmsa webhook for k8s version 1.22
  • Loading branch information
jsturtevant authored Jun 4, 2021
2 parents e13dafe + dc75308 commit ad12236
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 43 deletions.
5 changes: 3 additions & 2 deletions admission-webhook/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ SHELL := /bin/bash
WEBHOOK_ROOT := $(CURDIR)

DEV_IMAGE_NAME = k8s-windows-gmsa-webhook-dev
# FIXME: find a better way to distribute/publish this image
IMAGE_NAME = wk88/k8s-gmsa-webhook:latest
VERSION = $(shell git describe --tags --always `git rev-parse HEAD`)
IMAGE_REPO = sigwindowstools/k8s-gmsa-webhook
IMAGE_NAME = $(IMAGE_REPO):$(VERSION)

CURL = $(shell which curl 2> /dev/null)
WGET = $(shell which wget 2> /dev/null)
Expand Down
5 changes: 3 additions & 2 deletions admission-webhook/deploy/create-signed-cert.sh
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ EOF
gen_file gen_csr_conf "$CSR_CONF"

SERVER_CSR="$CERTS_DIR/server.csr"
gen_server_scr() { openssl req -new -key "$SERVER_KEY" -subj "/CN=$SERVICE.$NAMESPACE.svc" -out "$SERVER_CSR" -config "$CSR_CONF"; }
gen_server_scr() { openssl req -new -key "$SERVER_KEY" -subj "/O=system:nodes/CN=system:node:$SERVICE.$NAMESPACE.svc" -out "$SERVER_CSR" -config "$CSR_CONF"; }
gen_file gen_server_scr "$SERVER_CSR"

CSR_NAME="$SERVICE.$NAMESPACE"
Expand All @@ -109,14 +109,15 @@ fi

# create server cert/key CSR and send to k8s API
CSR_CONTENTS=$(cat <<EOF
apiVersion: certificates.k8s.io/v1beta1
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: $CSR_NAME
spec:
groups:
- system:authenticated
request: $(cat "$SERVER_CSR" | base64 -w 0)
signerName: kubernetes.io/kubelet-serving
usages:
- digital signature
- key encipherment
Expand Down
4 changes: 2 additions & 2 deletions admission-webhook/deploy/deploy-gmsa-webhook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ usage: $0 --file MANIFESTS_FILE [--name NAME] [--namespace NAMESPACE] [--image I
MANIFESTS_FILE is the path to the file the k8s manifests will be written to.
NAME defaults to 'gmsa-webhook' and is used in the names of most of the k8s resources created.
NAMESPACE is the namespace to deploy to; defaults to 'gmsa-webhook'.
IMAGE_NAME is the name of the Docker image containing the webhook; defaults to 'wk88/k8s-gmsa-webhook:latest' (FIXME: figure out a better way to distribute this image)
IMAGE_NAME is the name of the Docker image containing the webhook; defaults to 'sigwindowstools/k8s-gmsa-webhook:latest'
CERTS_DIR defaults to 'gmsa-webhook-certs'
If --dry-run is set, the script echoes what command it would perform
Expand Down Expand Up @@ -82,7 +82,7 @@ main() {
local MANIFESTS_FILE=
local NAME='gmsa-webhook'
local NAMESPACE='gmsa-webhook'
local IMAGE_NAME='wk88/k8s-gmsa-webhook:latest'
local IMAGE_NAME='sigwindowstools/k8s-gmsa-webhook:latest'
local CERTS_DIR='gmsa-webhook-certs'
local DRY_RUN=false
local OVERWRITE=false
Expand Down
54 changes: 46 additions & 8 deletions admission-webhook/deploy/gmsa-crd.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,55 @@
apiVersion: apiextensions.k8s.io/v1beta1
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: gmsacredentialspecs.windows.k8s.io
annotations:
"api-approved.kubernetes.io": "https://github.com/kubernetes/enhancements/tree/master/keps/sig-windows/689-windows-gmsa"
spec:
group: windows.k8s.io
version: v1alpha1
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
credspec:
description: GMSA Credential Spec
type: object
properties:
ActiveDirectoryConfig:
type: object
properties:
GroupManagedServiceAccounts:
type: array
items:
type: object
properties:
Name:
type: string
Scope:
type: string
CmsPlugins:
type: array
items:
type: string
DomainJoinConfig:
type: object
properties:
DnsName:
type: string
DnsTreeName:
type: string
Guid:
type: string
MachineAccountName:
type: string
NetBiosName:
type: string
Sid:
type: string
names:
kind: GMSACredentialSpec
plural: gmsacredentialspecs
scope: Cluster
validation:
openAPIV3Schema:
properties:
credspec:
description: GMSA Credential Spec
type: object
8 changes: 6 additions & 2 deletions admission-webhook/deploy/gmsa-webhook.yml.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ spec:

---

apiVersion: admissionregistration.k8s.io/v1beta1
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: ${NAME}
Expand All @@ -146,6 +146,8 @@ webhooks:
apiVersions: ["*"]
resources: ["pods"]
failurePolicy: Fail
admissionReviewVersions: ["v1", "v1beta1"]
sideEffects: None
# don't run on ${NAMESPACE}
namespaceSelector:
matchExpressions:
Expand All @@ -155,7 +157,7 @@ webhooks:

---

apiVersion: admissionregistration.k8s.io/v1beta1
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: ${NAME}
Expand All @@ -173,6 +175,8 @@ webhooks:
apiVersions: ["*"]
resources: ["pods"]
failurePolicy: Fail
admissionReviewVersions: ["v1", "v1beta1"]
sideEffects: None
# don't run on ${NAMESPACE}
namespaceSelector:
matchExpressions:
Expand Down
3 changes: 2 additions & 1 deletion admission-webhook/make/image.mk
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# must stay consistent with the go version defined in .travis.yml
GO_VERSION = 1.16
VERSION = $(shell git rev-parse HEAD)
DOCKER_BUILD = docker build . --build-arg GO_VERSION=$(GO_VERSION) --build-arg VERSION=$(VERSION)

.PHONY: image_build_dev
Expand All @@ -10,7 +9,9 @@ image_build_dev:
.PHONY: image_build
image_build:
$(DOCKER_BUILD) -f dockerfiles/Dockerfile -t $(IMAGE_NAME)
docker tag $(IMAGE_NAME) $(IMAGE_REPO):latest

.PHONY: image_push
image_push:
docker push $(IMAGE_NAME)
docker push $(IMAGE_REPO):latest
40 changes: 23 additions & 17 deletions admission-webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"time"

"github.com/sirupsen/logrus"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
admissionV1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -133,7 +133,13 @@ func (webhook *webhook) ServeHTTP(responseWriter http.ResponseWriter, request *h
}

admissionResponse := webhook.httpRequestToAdmissionResponse(request, operation)
responseAdmissionReview := admissionv1beta1.AdmissionReview{Response: admissionResponse}
responseAdmissionReview := admissionV1.AdmissionReview{
TypeMeta: metav1.TypeMeta{
Kind: "AdmissionReview",
APIVersion: "admission.k8s.io/v1",
},
Response: admissionResponse,
}

writeJSONBody(responseWriter, responseAdmissionReview)
}
Expand All @@ -159,7 +165,7 @@ func writeJSONBody(responseWriter http.ResponseWriter, jsonBody interface{}) {
}

// httpRequestToAdmissionResponse turns a raw HTTP request into an AdmissionResponse struct.
func (webhook *webhook) httpRequestToAdmissionResponse(request *http.Request, operation webhookOperation) *admissionv1beta1.AdmissionResponse {
func (webhook *webhook) httpRequestToAdmissionResponse(request *http.Request, operation webhookOperation) *admissionV1.AdmissionResponse {
// read the body
if request.Body == nil {
deniedAdmissionResponse(fmt.Errorf("no request body"), http.StatusBadRequest)
Expand All @@ -173,7 +179,7 @@ func (webhook *webhook) httpRequestToAdmissionResponse(request *http.Request, op
logrus.Debugf("handling %s request: %s", operation, body)

// unmarshall the request
admissionReview := admissionv1beta1.AdmissionReview{}
admissionReview := admissionV1.AdmissionReview{}
if err = json.Unmarshal(body, &admissionReview); err != nil {
return deniedAdmissionResponse(fmt.Errorf("unable to unmarshall JSON body as an admission review: %v", err), http.StatusBadRequest)
}
Expand All @@ -193,7 +199,7 @@ func (webhook *webhook) httpRequestToAdmissionResponse(request *http.Request, op
}

// validateOrMutate is where the non-HTTP-related work happens.
func (webhook *webhook) validateOrMutate(ctx context.Context, request *admissionv1beta1.AdmissionRequest, operation webhookOperation) (*admissionv1beta1.AdmissionResponse, *podAdmissionError) {
func (webhook *webhook) validateOrMutate(ctx context.Context, request *admissionV1.AdmissionRequest, operation webhookOperation) (*admissionV1.AdmissionResponse, *podAdmissionError) {
if request.Kind.Kind != "Pod" {
return nil, &podAdmissionError{error: fmt.Errorf("expected a Pod object, got a %v", request.Kind.Kind), code: http.StatusBadRequest}
}
Expand All @@ -204,7 +210,7 @@ func (webhook *webhook) validateOrMutate(ctx context.Context, request *admission
}

switch request.Operation {
case admissionv1beta1.Create:
case admissionV1.Create:
switch operation {
case validate:
return webhook.validateCreateRequest(ctx, pod, request.Namespace)
Expand All @@ -215,7 +221,7 @@ func (webhook *webhook) validateOrMutate(ctx context.Context, request *admission
panic(fmt.Errorf("unexpected webhook operation: %v", operation))
}

case admissionv1beta1.Update:
case admissionV1.Update:
if operation == validate {
oldPod, err := unmarshallPod(request.OldObject)
if err != nil {
Expand All @@ -225,7 +231,7 @@ func (webhook *webhook) validateOrMutate(ctx context.Context, request *admission
}

// we only do validation on updates, no mutation
return &admissionv1beta1.AdmissionResponse{Allowed: true}, nil
return &admissionV1.AdmissionResponse{Allowed: true}, nil
default:
return nil, &podAdmissionError{error: fmt.Errorf("unpexpected operation %s", request.Operation), pod: pod, code: http.StatusBadRequest}
}
Expand All @@ -244,7 +250,7 @@ func unmarshallPod(object runtime.RawExtension) (*corev1.Pod, *podAdmissionError
// validateCreateRequest ensures that the GMSA contents set in the pod's spec
// match the corresponding GMSA names, and that the pod's service account
// is authorized to `use` the requested GMSA's.
func (webhook *webhook) validateCreateRequest(ctx context.Context, pod *corev1.Pod, namespace string) (*admissionv1beta1.AdmissionResponse, *podAdmissionError) {
func (webhook *webhook) validateCreateRequest(ctx context.Context, pod *corev1.Pod, namespace string) (*admissionV1.AdmissionResponse, *podAdmissionError) {
if err := iterateOverWindowsSecurityOptions(pod, func(windowsOptions *corev1.WindowsSecurityContextOptions, resourceKind gmsaResourceKind, resourceName string, _ int) *podAdmissionError {
if credSpecName := windowsOptions.GMSACredentialSpecName; credSpecName != nil {
// let's check that the associated service account can read the relevant cred spec CRD
Expand Down Expand Up @@ -279,7 +285,7 @@ func (webhook *webhook) validateCreateRequest(ctx context.Context, pod *corev1.P
return nil, err
}

return &admissionv1beta1.AdmissionResponse{Allowed: true}, nil
return &admissionV1.AdmissionResponse{Allowed: true}, nil
}

// compareCredSpecContents returns true iff the two strings represent the same credential spec contents.
Expand Down Expand Up @@ -307,7 +313,7 @@ func compareCredSpecContents(fromResource, fromCRD string) (bool, error) {
}

// mutateCreateRequest inlines the requested GMSA's into the pod's and containers' `WindowsSecurityOptions` structs.
func (webhook *webhook) mutateCreateRequest(ctx context.Context, pod *corev1.Pod) (*admissionv1beta1.AdmissionResponse, *podAdmissionError) {
func (webhook *webhook) mutateCreateRequest(ctx context.Context, pod *corev1.Pod) (*admissionV1.AdmissionResponse, *podAdmissionError) {
var patches []map[string]string

if err := iterateOverWindowsSecurityOptions(pod, func(windowsOptions *corev1.WindowsSecurityContextOptions, resourceKind gmsaResourceKind, resourceName string, containerIndex int) *podAdmissionError {
Expand Down Expand Up @@ -341,7 +347,7 @@ func (webhook *webhook) mutateCreateRequest(ctx context.Context, pod *corev1.Pod
return nil, err
}

admissionResponse := &admissionv1beta1.AdmissionResponse{Allowed: true}
admissionResponse := &admissionV1.AdmissionResponse{Allowed: true}

if len(patches) != 0 {
patchesBytes, err := json.Marshal(patches)
Expand All @@ -350,15 +356,15 @@ func (webhook *webhook) mutateCreateRequest(ctx context.Context, pod *corev1.Pod
}

admissionResponse.Patch = patchesBytes
patchType := admissionv1beta1.PatchTypeJSONPatch
patchType := admissionV1.PatchTypeJSONPatch
admissionResponse.PatchType = &patchType
}

return admissionResponse, nil
}

// validateUpdateRequest ensures that there are no updates to any of the GMSA names or contents.
func validateUpdateRequest(pod, oldPod *corev1.Pod) (*admissionv1beta1.AdmissionResponse, *podAdmissionError) {
func validateUpdateRequest(pod, oldPod *corev1.Pod) (*admissionV1.AdmissionResponse, *podAdmissionError) {
var oldPodContainerOptions map[string]*corev1.WindowsSecurityContextOptions

if err := iterateOverWindowsSecurityOptions(pod, func(windowsOptions *corev1.WindowsSecurityContextOptions, resourceKind gmsaResourceKind, resourceName string, _ int) *podAdmissionError {
Expand Down Expand Up @@ -405,7 +411,7 @@ func validateUpdateRequest(pod, oldPod *corev1.Pod) (*admissionv1beta1.Admission
return nil, err
}

return &admissionv1beta1.AdmissionResponse{Allowed: true}, nil
return &admissionV1.AdmissionResponse{Allowed: true}, nil
}

func equalStringPointers(s1, s2 *string) bool {
Expand Down Expand Up @@ -444,7 +450,7 @@ func iterateOverWindowsSecurityOptions(pod *corev1.Pod, f func(windowsOptions *c

// deniedAdmissionResponse is a helper function to create an AdmissionResponse
// with an embedded error.
func deniedAdmissionResponse(err error, httpCode ...int) *admissionv1beta1.AdmissionResponse {
func deniedAdmissionResponse(err error, httpCode ...int) *admissionV1.AdmissionResponse {
var code int
logMsg := "refusing to admit"

Expand All @@ -465,7 +471,7 @@ func deniedAdmissionResponse(err error, httpCode ...int) *admissionv1beta1.Admis

logrus.Infof("%s: %v", logMsg, err)

return &admissionv1beta1.AdmissionResponse{
return &admissionV1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Message: err.Error(),
Expand Down
14 changes: 7 additions & 7 deletions admission-webhook/webhook_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
admissionV1 "k8s.io/api/admission/v1"
authenticationv1 "k8s.io/api/authentication/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -28,8 +28,8 @@ func TestHTTPWebhook(t *testing.T) {

pod := buildPod(dummyServiceAccoutName, buildWindowsOptions(dummyCredSpecName, ""), map[string]*corev1.WindowsSecurityContextOptions{"container-name": nil})

admissionRequest := &admissionv1beta1.AdmissionReview{
Request: &admissionv1beta1.AdmissionRequest{
admissionRequest := &admissionV1.AdmissionReview{
Request: &admissionV1.AdmissionRequest{
UID: requestUID,
Kind: metav1.GroupVersionKind{
Version: "v1",
Expand All @@ -40,7 +40,7 @@ func TestHTTPWebhook(t *testing.T) {
Resource: "pods",
},
Namespace: dummyNamespace,
Operation: admissionv1beta1.Create,
Operation: admissionV1.Create,
UserInfo: authenticationv1.UserInfo{
Username: "system:serviceaccount:kube-system:replicaset-controller",
UID: "cb335ac0-34b4-11e9-9745-06da3a0adce4",
Expand Down Expand Up @@ -83,7 +83,7 @@ func TestHTTPWebhook(t *testing.T) {
assert.True(t, response.Response.Allowed)

if assert.NotNil(t, response.Response.PatchType) {
assert.Equal(t, admissionv1beta1.PatchTypeJSONPatch, *response.Response.PatchType)
assert.Equal(t, admissionV1.PatchTypeJSONPatch, *response.Response.PatchType)
}

var patches []map[string]string
Expand Down Expand Up @@ -190,7 +190,7 @@ func startHTTPServer(t *testing.T, kubeClient *dummyKubeClient) (int, func()) {
}
}

func makeHTTPRequest(t *testing.T, port int, method string, path string, admissionRequest *admissionv1beta1.AdmissionReview, headers ...string) (httpCode int, admissionResponse *admissionv1beta1.AdmissionReview) {
func makeHTTPRequest(t *testing.T, port int, method string, path string, admissionRequest *admissionV1.AdmissionReview, headers ...string) (httpCode int, admissionResponse *admissionV1.AdmissionReview) {
require.Equal(t, 0, len(headers)%2, "header names and values should be provided in pairs")

reqBody, err := json.Marshal(admissionRequest)
Expand All @@ -217,7 +217,7 @@ func makeHTTPRequest(t *testing.T, port int, method string, path string, admissi
respBody, err := ioutil.ReadAll(resp.Body)
require.Nil(t, err)

admissionResponse = &admissionv1beta1.AdmissionReview{}
admissionResponse = &admissionV1.AdmissionReview{}
if err := json.Unmarshal(respBody, admissionResponse); err != nil {
admissionResponse = nil
}
Expand Down
Loading

0 comments on commit ad12236

Please sign in to comment.