Skip to content

Commit

Permalink
Create random hostname for GMSA
Browse files Browse the repository at this point in the history
This will only apply to gmsa pods which have the corresponding security context

Disabling/enabling of this can be controlled through ENV
  • Loading branch information
zylxjtu committed Oct 17, 2024
1 parent 68be831 commit d7d9cb8
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 22 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ jobs:
env:
T: integration
DEPLOY_METHOD: chart
integration-rotation-enabled:
integration-optional-features:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
Expand All @@ -126,5 +126,5 @@ jobs:
env:
T: integration
DEPLOY_METHOD: chart
HELM_INSTALL_FLAGS_FLAGS: --set certificates.certReload.enabled=true
HELM_INSTALL_FLAGS_FLAGS: --set certificates.certReload.enabled=true, --set randomHostname=true

64 changes: 64 additions & 0 deletions admission-webhook/integration_tests/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,70 @@ func TestPossibleToUpdatePodWithNewCert(t *testing.T) {
assert.Equal(t, expectedCredSpec0, extractContainerCredSpecContents(t, pod3, testName3))
}

func TestPossibleHostnameRandomization(t *testing.T) {
deployMethod := os.Getenv("DEPLOY_METHOD")
if deployMethod != "chart" {
t.Skip("Non chart deployment method not supported for this test")
}

webHookNs := os.Getenv("NAMESPACE")
webHookDeploymentName := os.Getenv("DEPLOYMENT_NAME")
webhook, err := kubeClient(t).AppsV1().Deployments(webHookNs).Get(context.Background(), webHookDeploymentName, metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}

randomHostnameEnabled := false
for _, envVar := range webhook.Spec.Template.Spec.Containers[0].Env {
if strings.EqualFold(envVar.Name, "RANDOM_HOSTNAME") && strings.EqualFold(envVar.Value, "true") {
randomHostnameEnabled = true
}
}

if randomHostnameEnabled {
testName1 := "happy-path-with-hostname-randomization"
credSpecTemplates1 := []string{"credspec-0"}
templates1 := []string{"credspecs-users-rbac-role", "service-account", "sa-rbac-binding", "simple-with-gmsa"}

testConfig1, tearDownFunc1 := integrationTestSetup(t, testName1, credSpecTemplates1, templates1)
defer tearDownFunc1()

pod := waitForPodToComeUp(t, testConfig1.Namespace, "app="+testName1)
assert.NotEqual(t, testName1, pod.Spec.Hostname)
assert.Equal(t, 15, len(pod.Spec.Hostname))

testName2 := "hostnameset-no-hostname-randomization"
credSpecTemplates2 := []string{"credspec-0"}
templates2 := []string{"credspecs-users-rbac-role", "service-account", "sa-rbac-binding", "simple-with-gmsa-hostname"}

testConfig2, tearDownFunc2 := integrationTestSetup(t, testName2, credSpecTemplates2, templates2)
defer tearDownFunc2()

pod = waitForPodToComeUp(t, testConfig2.Namespace, "app="+testName2)
assert.Equal(t, testName2, pod.Spec.Hostname)

testName3 := "nogmsa-hostname-randomization"
credSpecTemplates3 := []string{"credspec-0"}
templates3 := []string{"credspecs-users-rbac-role", "service-account", "sa-rbac-binding", "simple-without-gmsa"}

testConfig3, tearDownFunc3 := integrationTestSetup(t, testName3, credSpecTemplates3, templates3)
defer tearDownFunc3()
pod = waitForPodToComeUp(t, testConfig3.Namespace, "app="+testName3)

assert.Equal(t, "", pod.Spec.Hostname)
} else {
testName4 := "notenabled-hostname-randomization"
credSpecTemplates4 := []string{"credspec-0"}
templates4 := []string{"credspecs-users-rbac-role", "service-account", "sa-rbac-binding", "simple-with-gmsa"}

testConfig4, tearDownFunc4 := integrationTestSetup(t, testName4, credSpecTemplates4, templates4)
defer tearDownFunc4()
pod := waitForPodToComeUp(t, testConfig4.Namespace, "app="+testName4)

assert.Equal(t, "", pod.Spec.Hostname)
}
}

/* Helpers */

type testConfig struct {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## a simple deployment with a pod-level GMSA cred spec

apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: {{ .TestName }}
name: {{ .TestName }}
namespace: {{ .Namespace }}
spec:
replicas: 1
selector:
matchLabels:
app: {{ .TestName }}
template:
metadata:
labels:
app: {{ .TestName }}
spec:
hostname: {{ .TestName }}
serviceAccountName: {{ .ServiceAccountName }}
securityContext:
windowsOptions:
gmsaCredentialSpecName: {{ index .CredSpecNames 0 }}
containers:
- image: registry.k8s.io/pause
name: nginx
{{- range $line := .ExtraSpecLines }}
{{ $line }}
{{- end }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## a simple deployment with a pod-level GMSA cred spec

apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: {{ .TestName }}
name: {{ .TestName }}
namespace: {{ .Namespace }}
spec:
replicas: 1
selector:
matchLabels:
app: {{ .TestName }}
template:
metadata:
labels:
app: {{ .TestName }}
spec:
serviceAccountName: {{ .ServiceAccountName }}
containers:
- image: registry.k8s.io/pause
name: nginx
{{- range $line := .ExtraSpecLines }}
{{ $line }}
{{- end }}
20 changes: 19 additions & 1 deletion admission-webhook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ func main() {
panic(err)
}

webhook := newWebhookWithOptions(kubeClient, WithCertReload(*enableCertReload))
options := []WebhookOption{WithCertReload(*enableCertReload)}

randomHostname := env_bool("RANDOM_HOSTNAME")
options = append(options, WithRandomHostname(randomHostname))

webhook := newWebhookWithOptions(kubeClient, options...)

tlsConfig := &tlsConfig{
crtPath: env("TLS_CRT"),
Expand Down Expand Up @@ -98,6 +103,19 @@ func env_float(key string, defaultFloat float32) float32 {
return defaultFloat
}

func env_bool(key string) bool {
if v, found := os.LookupEnv(key); found {
// Convert string to bool
if boolValue, err := strconv.ParseBool(v); err == nil {
return boolValue
}
logrus.Warningf("unable to parse environment variable %s with value %s to bool, use default value false", key, v)
}

// return bool default value: false
return false
}

func env_int(key string, defaultInt int) int {
if v, found := os.LookupEnv(key); found {
if i, err := strconv.Atoi(v); err == nil {
Expand Down
26 changes: 22 additions & 4 deletions admission-webhook/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ type dummyKubeClient struct {
retrieveCredSpecContentsFunc func(ctx context.Context, credSpecName string) (contents string, httpCode int, err error)
}


func (dkc *dummyKubeClient) isAuthorizedToUseCredSpec(ctx context.Context, serviceAccountName, namespace, credSpecName string) (authorized bool, reason string) {
if dkc.isAuthorizedToUseCredSpecFunc != nil {
return dkc.isAuthorizedToUseCredSpecFunc(ctx, serviceAccountName, namespace, credSpecName)
Expand Down Expand Up @@ -59,6 +58,14 @@ func setWindowsOptions(winOptions *corev1.WindowsSecurityContextOptions, credSpe
// case a `*corev1.WindowsSecurityContextOptions` is built using that string as the name of the cred spec to use.
// Same goes for the values of `containerNamesAndWindowsOptions`.
func buildPod(serviceAccountName string, podWindowsOptions *corev1.WindowsSecurityContextOptions, containerNamesAndWindowsOptions map[string]*corev1.WindowsSecurityContextOptions) *corev1.Pod {
return buildPodWithHostName(serviceAccountName, nil, podWindowsOptions, containerNamesAndWindowsOptions)
}

// buildPod builds a pod for unit tests.
// `podWindowsOptions` should be either a full `*corev1.WindowsSecurityContextOptions` or a string, in which
// case a `*corev1.WindowsSecurityContextOptions` is built using that string as the name of the cred spec to use.
// Same goes for the values of `containerNamesAndWindowsOptions`.
func buildPodWithHostName(serviceAccountName string, hostname *string, podWindowsOptions *corev1.WindowsSecurityContextOptions, containerNamesAndWindowsOptions map[string]*corev1.WindowsSecurityContextOptions) *corev1.Pod {
containers := make([]corev1.Container, len(containerNamesAndWindowsOptions))
i := 0
for name, winOptions := range containerNamesAndWindowsOptions {
Expand All @@ -70,10 +77,21 @@ func buildPod(serviceAccountName string, podWindowsOptions *corev1.WindowsSecuri
}

shuffleContainers(containers)
podSpec := corev1.PodSpec{
ServiceAccountName: serviceAccountName,
Containers: containers,

var podSpec corev1.PodSpec
if hostname != nil {
podSpec = corev1.PodSpec{
ServiceAccountName: serviceAccountName,
Containers: containers,
Hostname: *hostname,
}
} else {
podSpec = corev1.PodSpec{
ServiceAccountName: serviceAccountName,
Containers: containers,
}
}

if podWindowsOptions != nil {
podSpec.SecurityContext = &corev1.PodSecurityContext{WindowsOptions: podWindowsOptions}
}
Expand Down
53 changes: 44 additions & 9 deletions admission-webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"strings"
"time"

"github.com/google/uuid"

"github.com/sirupsen/logrus"
admissionV1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -48,7 +50,8 @@ type podAdmissionError struct {
}

type WebhookConfig struct {
EnableCertReload bool
EnableCertReload bool
EnableRandomHostName bool
}

type WebhookOption func(*WebhookConfig)
Expand All @@ -59,12 +62,18 @@ func WithCertReload(enabled bool) WebhookOption {
}
}

func WithRandomHostname(enabled bool) WebhookOption {
return func(cfg *WebhookConfig) {
cfg.EnableRandomHostName = enabled
}
}

func newWebhook(client kubeClientInterface) *webhook {
return newWebhookWithOptions(client)
}

func newWebhookWithOptions(client kubeClientInterface, options ...WebhookOption) *webhook {
config := &WebhookConfig{EnableCertReload: false}
config := &WebhookConfig{EnableCertReload: false, EnableRandomHostName: false}

for _, option := range options {
option(config)
Expand Down Expand Up @@ -358,9 +367,11 @@ 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) (*admissionV1.AdmissionResponse, *podAdmissionError) {
var patches []map[string]string
hasGMSA := false

if err := iterateOverWindowsSecurityOptions(pod, func(windowsOptions *corev1.WindowsSecurityContextOptions, resourceKind gmsaResourceKind, resourceName string, containerIndex int) *podAdmissionError {
if credSpecName := windowsOptions.GMSACredentialSpecName; credSpecName != nil {
hasGMSA = true
// if the user has pre-set the GMSA's contents, we won't override it - it'll be down
// to the validation endpoint to make sure the contents actually are what they should
if credSpecContents := windowsOptions.GMSACredentialSpec; credSpecContents == nil {
Expand Down Expand Up @@ -392,15 +403,31 @@ func (webhook *webhook) mutateCreateRequest(ctx context.Context, pod *corev1.Pod

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

if len(patches) != 0 {
patchesBytes, err := json.Marshal(patches)
if err != nil {
return nil, &podAdmissionError{error: fmt.Errorf("unable to marshall patch JSON %v: %v", patches, err), pod: pod, code: http.StatusInternalServerError}
if hasGMSA && webhook.config.EnableRandomHostName {
// Pods are GMSA related, Env enabled, patch the hostname only if it is empty
hostName := pod.Spec.Hostname
if hostName == "" {
hostName = generateUUID()
patches = append(patches, map[string]string{
"op": "add",
"path": "/spec/hostname",
"value": hostName,
})
} else {
// Will honor the hostname set in the spec, print out a message
logrus.Warnf("hostname is set in spec and will be hornored instead of being randomized")
}

admissionResponse.Patch = patchesBytes
patchType := admissionV1.PatchTypeJSONPatch
admissionResponse.PatchType = &patchType
if len(patches) != 0 {
patchesBytes, err := json.Marshal(patches)
if err != nil {
return nil, &podAdmissionError{error: fmt.Errorf("unable to marshall patch JSON %v: %v", patches, err), pod: pod, code: http.StatusInternalServerError}
}

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

return admissionResponse, nil
Expand Down Expand Up @@ -537,3 +564,11 @@ func (ln tcpKeepAliveListener) Accept() (net.Conn, error) {
tc.SetKeepAlivePeriod(3 * time.Minute)
return tc, nil
}

func generateUUID() string {
// Generate a new UUID
id := uuid.New()
// Convert to string and get the first 15 characters in lower case
shortUUID := strings.ToLower(id.String()[:15])
return shortUUID
}
Loading

0 comments on commit d7d9cb8

Please sign in to comment.